From 5571f6b1a83af586a2a82610a37ffa8e8c9ea216 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Mon, 19 Aug 2019 22:56:40 +0200 Subject: [PATCH] Started working on new UI. --- src/core/data_formats/parsing/ninja/index.ts | 2 +- src/core/rendering/Renderer.ts | 11 +- .../rendering/conversion/ninja_geometry.ts | 13 +- src/core/ui/RendererComponent.tsx | 3 +- src/index.tsx | 19 +- src/new/application/gui/ApplicationView.ts | 23 ++ src/new/application/gui/MainContentView.ts | 47 +++ src/new/application/gui/NavigationView.css | 31 ++ src/new/application/gui/NavigationView.ts | 72 +++++ src/new/core/gui/Button.css | 20 ++ src/new/core/gui/Button.ts | 13 + src/new/core/gui/Disposable.ts | 3 + src/new/core/gui/LazyView.ts | 46 +++ src/new/core/gui/RendererView.ts | 26 ++ src/new/core/gui/Resizable.ts | 3 + src/new/core/gui/ResizableView.ts | 15 + src/new/core/gui/TabContainer.css | 25 ++ src/new/core/gui/TabContainer.ts | 94 ++++++ src/new/core/gui/ToolBar.css | 25 ++ src/new/core/gui/ToolBar.ts | 19 ++ src/new/core/gui/View.ts | 17 ++ src/new/core/gui/dom.ts | 20 ++ src/new/core/observable/Observable.ts | 45 +++ src/new/core/stores/GuiStore.ts | 48 +++ src/new/index.css | 42 +++ src/new/index.ts | 27 ++ .../viewer/domain/CharacterClassAnimation.ts | 3 + src/new/viewer/domain/CharacterClassModel.ts | 8 + src/new/viewer/gui/ModelView.css | 26 ++ src/new/viewer/gui/ModelView.ts | 123 ++++++++ src/new/viewer/gui/TextureView.ts | 6 + src/new/viewer/gui/ViewerView.ts | 29 ++ src/new/viewer/rendering/ModelRenderer.ts | 143 +++++++++ src/new/viewer/stores/ModelStore.ts | 286 ++++++++++++++++++ src/quest_editor/rendering/QuestRenderer.ts | 13 +- src/viewer/loading/player.ts | 2 +- src/viewer/rendering/ModelRenderer.ts | 11 +- src/viewer/rendering/TextureRenderer.ts | 15 +- tsconfig.json | 2 +- webpack.dev.js | 6 +- 40 files changed, 1339 insertions(+), 43 deletions(-) create mode 100644 src/new/application/gui/ApplicationView.ts create mode 100644 src/new/application/gui/MainContentView.ts create mode 100644 src/new/application/gui/NavigationView.css create mode 100644 src/new/application/gui/NavigationView.ts create mode 100644 src/new/core/gui/Button.css create mode 100644 src/new/core/gui/Button.ts create mode 100644 src/new/core/gui/Disposable.ts create mode 100644 src/new/core/gui/LazyView.ts create mode 100644 src/new/core/gui/RendererView.ts create mode 100644 src/new/core/gui/Resizable.ts create mode 100644 src/new/core/gui/ResizableView.ts create mode 100644 src/new/core/gui/TabContainer.css create mode 100644 src/new/core/gui/TabContainer.ts create mode 100644 src/new/core/gui/ToolBar.css create mode 100644 src/new/core/gui/ToolBar.ts create mode 100644 src/new/core/gui/View.ts create mode 100644 src/new/core/gui/dom.ts create mode 100644 src/new/core/observable/Observable.ts create mode 100644 src/new/core/stores/GuiStore.ts create mode 100644 src/new/index.css create mode 100644 src/new/index.ts create mode 100644 src/new/viewer/domain/CharacterClassAnimation.ts create mode 100644 src/new/viewer/domain/CharacterClassModel.ts create mode 100644 src/new/viewer/gui/ModelView.css create mode 100644 src/new/viewer/gui/ModelView.ts create mode 100644 src/new/viewer/gui/TextureView.ts create mode 100644 src/new/viewer/gui/ViewerView.ts create mode 100644 src/new/viewer/rendering/ModelRenderer.ts create mode 100644 src/new/viewer/stores/ModelStore.ts diff --git a/src/core/data_formats/parsing/ninja/index.ts b/src/core/data_formats/parsing/ninja/index.ts index b8b257bc..ebdb6052 100644 --- a/src/core/data_formats/parsing/ninja/index.ts +++ b/src/core/data_formats/parsing/ninja/index.ts @@ -18,7 +18,7 @@ export function is_xj_model(model: NjModel): model is XjModel { return model.type === "xj"; } -export class NjObject { +export class NjObject { evaluation_flags: NjEvaluationFlags; model: M | undefined; position: Vec3; diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 3cf01420..20700723 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -1,6 +1,7 @@ import CameraControls from "camera-controls"; import * as THREE from "three"; import { + Camera, Clock, Color, Group, @@ -21,7 +22,7 @@ CameraControls.install({ }, }); -export abstract class Renderer { +export abstract class Renderer { protected _debug = false; get debug(): boolean { @@ -32,7 +33,7 @@ export abstract class Renderer this._debug = debug; } - readonly camera: C; + readonly camera: Camera; readonly controls: CameraControls; readonly scene = new Scene(); readonly light_holder = new Group(); @@ -43,7 +44,7 @@ export abstract class Renderer private light = new HemisphereLight(0xffffff, 0x505050, 1.2); private controls_clock = new Clock(); - protected constructor(camera: C) { + protected constructor(camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera; this.dom_element.tabIndex = 0; @@ -100,6 +101,10 @@ export abstract class Renderer ); } + dispose(): void { + this.renderer.dispose(); + } + protected render(): void { this.renderer.render(this.scene, this.camera); } diff --git a/src/core/rendering/conversion/ninja_geometry.ts b/src/core/rendering/conversion/ninja_geometry.ts index 45256a7d..afe71924 100644 --- a/src/core/rendering/conversion/ninja_geometry.ts +++ b/src/core/rendering/conversion/ninja_geometry.ts @@ -11,14 +11,11 @@ const NO_TRANSLATION = new Vector3(0, 0, 0); const NO_ROTATION = new Quaternion(0, 0, 0, 1); const NO_SCALE = new Vector3(1, 1, 1); -export function ninja_object_to_geometry_builder( - object: NjObject, - builder: GeometryBuilder, -): void { +export function ninja_object_to_geometry_builder(object: NjObject, builder: GeometryBuilder): void { new GeometryCreator(builder).to_geometry_builder(object); } -export function ninja_object_to_buffer_geometry(object: NjObject): BufferGeometry { +export function ninja_object_to_buffer_geometry(object: NjObject): BufferGeometry { return new GeometryCreator(new GeometryBuilder()).create_buffer_geometry(object); } @@ -62,17 +59,17 @@ class GeometryCreator { this.builder = builder; } - to_geometry_builder(object: NjObject): void { + to_geometry_builder(object: NjObject): void { this.object_to_geometry(object, undefined, new Matrix4()); } - create_buffer_geometry(object: NjObject): BufferGeometry { + create_buffer_geometry(object: NjObject): BufferGeometry { this.to_geometry_builder(object); return this.builder.build(); } private object_to_geometry( - object: NjObject, + object: NjObject, parent_bone: Bone | undefined, parent_matrix: Matrix4, ): void { diff --git a/src/core/ui/RendererComponent.tsx b/src/core/ui/RendererComponent.tsx index 414da616..b26e897b 100644 --- a/src/core/ui/RendererComponent.tsx +++ b/src/core/ui/RendererComponent.tsx @@ -1,9 +1,8 @@ import React, { Component, ReactNode } from "react"; -import { OrthographicCamera, PerspectiveCamera } from "three"; import { Renderer } from "../rendering/Renderer"; type Props = { - renderer: Renderer; + renderer: Renderer; width: number; height: number; debug?: boolean; diff --git a/src/index.tsx b/src/index.tsx index f0b81df7..2fa9c9e7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,12 +3,13 @@ import ReactDOM from "react-dom"; import Logger from "js-logger"; import styles from "./core/ui/index.css"; import { ApplicationComponent } from "./application/ui/ApplicationComponent"; -import "react-virtualized/styles.css"; -import "react-select/dist/react-select.css"; -import "react-virtualized-select/styles.css"; +// import "react-virtualized/styles.css"; +// import "react-select/dist/react-select.css"; +// import "react-virtualized-select/styles.css"; import "golden-layout/src/css/goldenlayout-base.css"; import "golden-layout/src/css/goldenlayout-dark-theme.css"; -import "antd/dist/antd.less"; +// import "antd/dist/antd.less"; +import { initialize } from "./new"; Logger.useDefaults({ defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], @@ -31,8 +32,10 @@ document.addEventListener("beforeinput", e => { } }); -const root_element = document.createElement("div"); -root_element.id = styles.phantasmal_world_root; -document.body.append(root_element); +// const root_element = document.createElement("div"); +// root_element.id = styles.phantasmal_world_root; +// document.body.append(root_element); +// +// ReactDOM.render(, root_element); -ReactDOM.render(, root_element); +initialize(); diff --git a/src/new/application/gui/ApplicationView.ts b/src/new/application/gui/ApplicationView.ts new file mode 100644 index 00000000..22b5f795 --- /dev/null +++ b/src/new/application/gui/ApplicationView.ts @@ -0,0 +1,23 @@ +import { NavigationView } from "./NavigationView"; +import { MainContentView } from "./MainContentView"; +import { create_el } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; + +export class ApplicationView extends ResizableView { + element = create_el("div", "application_ApplicationView"); + + private menu_view = this.disposable(new NavigationView()); + private main_content_view = this.disposable(new MainContentView()); + + constructor() { + super(); + + this.element.append(this.menu_view.element, this.main_content_view.element); + } + + resize(width: number, height: number): this { + super.resize(width, height); + this.main_content_view.resize(width, height - this.menu_view.height); + return this; + } +} diff --git a/src/new/application/gui/MainContentView.ts b/src/new/application/gui/MainContentView.ts new file mode 100644 index 00000000..c0acde22 --- /dev/null +++ b/src/new/application/gui/MainContentView.ts @@ -0,0 +1,47 @@ +import { create_el } from "../../core/gui/dom"; +import { View } from "../../core/gui/View"; +import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { LazyView } from "../../core/gui/LazyView"; +import { Resizable } from "../../core/gui/Resizable"; +import { ResizableView } from "../../core/gui/ResizableView"; + +const TOOLS: [GuiTool, () => Promise][] = [ + [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], +]; + +export class MainContentView extends ResizableView { + element = create_el("div", "application_MainContentView"); + + private tool_views = new Map( + TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]), + ); + + constructor() { + super(); + + for (const tool_view of this.tool_views.values()) { + this.element.append(tool_view.element); + } + + this.tool_changed(gui_store.tool, gui_store.tool); + this.disposable(gui_store.tool_prop.observe(this.tool_changed)); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + for (const tool_view of this.tool_views.values()) { + tool_view.resize(width, height); + } + + return this; + } + + private tool_changed = (new_tool: GuiTool, old_tool: GuiTool) => { + const old_view = this.tool_views.get(old_tool); + if (old_view) old_view.visible = false; + + const new_view = this.tool_views.get(new_tool); + if (new_view) new_view.visible = true; + }; +} diff --git a/src/new/application/gui/NavigationView.css b/src/new/application/gui/NavigationView.css new file mode 100644 index 00000000..c2a752e0 --- /dev/null +++ b/src/new/application/gui/NavigationView.css @@ -0,0 +1,31 @@ +.application_NavigationView { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: stretch; + background-color: hsl(0, 0%, 12%); + border-bottom: solid 2px var(--bg-color); +} + +.application_ToolButton input { + display: none; +} + +.application_ToolButton label { + box-sizing: border-box; + display: inline-block; + font-size: 16px; + height: 100%; + padding: 0 20px; + line-height: 40px; +} + +.application_ToolButton label:hover { + color: hsl(200, 25%, 85%); + background-color: hsl(0, 0%, 16%); +} + +.application_ToolButton input:checked + label { + color: hsl(200, 50%, 85%); + background-color: hsl(0, 0%, 20%); +} diff --git a/src/new/application/gui/NavigationView.ts b/src/new/application/gui/NavigationView.ts new file mode 100644 index 00000000..15ac1f4c --- /dev/null +++ b/src/new/application/gui/NavigationView.ts @@ -0,0 +1,72 @@ +import { create_el } from "../../core/gui/dom"; +import "./NavigationView.css"; +import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { View } from "../../core/gui/View"; + +const TOOLS: [GuiTool, string][] = [ + [GuiTool.Viewer, "Viewer"], + [GuiTool.QuestEditor, "Quest Editor"], + [GuiTool.HuntOptimizer, "Hunt Optimizer"], +]; + +export class NavigationView extends View { + element = create_el("div", "application_NavigationView"); + height = 40; + + private buttons = new Map( + TOOLS.map(([value, text]) => [value, this.disposable(new ToolButton(value, text))]), + ); + + constructor() { + super(); + + this.element.style.height = `${this.height}px`; + this.element.onclick = this.click; + + for (const button of this.buttons.values()) { + this.element.append(button.element); + } + + this.tool_changed(gui_store.tool); + this.disposable(gui_store.tool_prop.observe(this.tool_changed)); + } + + private click(e: MouseEvent): void { + if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { + gui_store.tool = (GuiTool as any)[e.target.control.value]; + } + } + + private tool_changed = (tool: GuiTool) => { + const button = this.buttons.get(tool); + if (button) button.checked = true; + }; +} + +class ToolButton extends View { + element: HTMLElement = create_el("span"); + + private input: HTMLInputElement = create_el("input"); + private label: HTMLLabelElement = create_el("label"); + + constructor(tool: GuiTool, text: string) { + super(); + + const tool_str = GuiTool[tool]; + + this.input.type = "radio"; + this.input.name = "application_ToolButton"; + this.input.value = tool_str; + this.input.id = `application_ToolButton_${tool_str}`; + + this.label.append(text); + this.label.htmlFor = `application_ToolButton_${tool_str}`; + + this.element.className = "application_ToolButton"; + this.element.append(this.input, this.label); + } + + set checked(checked: boolean) { + this.input.checked = checked; + } +} diff --git a/src/new/core/gui/Button.css b/src/new/core/gui/Button.css new file mode 100644 index 00000000..4c496445 --- /dev/null +++ b/src/new/core/gui/Button.css @@ -0,0 +1,20 @@ +.core_Button { + box-sizing: border-box; + background-color: #404040; + height: 26px; + padding: 2px 8px; + border: solid 1px #606060; + color: #f0f0f0; + outline: none; +} + +.core_Button:hover { + background-color: #505050; + border-color: #707070; +} + +.core_Button:active { + background-color: #404040; + border-color: #606060; + color: #e0e0e0; +} diff --git a/src/new/core/gui/Button.ts b/src/new/core/gui/Button.ts new file mode 100644 index 00000000..35af9635 --- /dev/null +++ b/src/new/core/gui/Button.ts @@ -0,0 +1,13 @@ +import { create_el } from "./dom"; +import { View } from "./View"; +import "./Button.css"; + +export class Button extends View { + element: HTMLButtonElement = create_el("button", "core_Button"); + + constructor(text: string) { + super(); + + this.element.textContent = text; + } +} diff --git a/src/new/core/gui/Disposable.ts b/src/new/core/gui/Disposable.ts new file mode 100644 index 00000000..3cbd9d2a --- /dev/null +++ b/src/new/core/gui/Disposable.ts @@ -0,0 +1,3 @@ +export interface Disposable { + dispose(): void; +} diff --git a/src/new/core/gui/LazyView.ts b/src/new/core/gui/LazyView.ts new file mode 100644 index 00000000..c1a89b77 --- /dev/null +++ b/src/new/core/gui/LazyView.ts @@ -0,0 +1,46 @@ +import { View } from "./View"; +import { create_el } from "./dom"; +import { Resizable } from "./Resizable"; +import { ResizableView } from "./ResizableView"; + +export class LazyView extends ResizableView { + element = create_el("div", "core_LazyView"); + + private _visible = false; + + set visible(visible: boolean) { + if (this._visible !== visible) { + this._visible = visible; + this.element.hidden = !visible; + + if (visible && !this.initialized) { + this.initialized = true; + + this.create_view().then(view => { + this.view = this.disposable(view); + this.view.resize(this.width, this.height); + this.element.append(view.element); + }); + } + } + } + + private initialized = false; + private view: View & Resizable | undefined; + + constructor(private create_view: () => Promise) { + super(); + + this.element.hidden = true; + } + + resize(width: number, height: number): this { + super.resize(width, height); + + if (this.view) { + this.view.resize(width, height); + } + + return this; + } +} diff --git a/src/new/core/gui/RendererView.ts b/src/new/core/gui/RendererView.ts new file mode 100644 index 00000000..2f685279 --- /dev/null +++ b/src/new/core/gui/RendererView.ts @@ -0,0 +1,26 @@ +import { ResizableView } from "./ResizableView"; +import { create_el } from "./dom"; +import { Renderer } from "../../../core/rendering/Renderer"; + +export class RendererView extends ResizableView { + readonly element = create_el("div"); + + constructor(private renderer: Renderer) { + super(); + + this.element.append(renderer.dom_element); + + this.disposable(renderer); + + // TODO: stop on hidden + renderer.start_rendering(); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + this.renderer.set_size(width, height); + + return this; + } +} diff --git a/src/new/core/gui/Resizable.ts b/src/new/core/gui/Resizable.ts new file mode 100644 index 00000000..14d665d2 --- /dev/null +++ b/src/new/core/gui/Resizable.ts @@ -0,0 +1,3 @@ +export interface Resizable { + resize(width: number, height: number): this; +} diff --git a/src/new/core/gui/ResizableView.ts b/src/new/core/gui/ResizableView.ts new file mode 100644 index 00000000..c8187401 --- /dev/null +++ b/src/new/core/gui/ResizableView.ts @@ -0,0 +1,15 @@ +import { View } from "./View"; +import { Resizable } from "./Resizable"; + +export abstract class ResizableView extends View implements Resizable { + protected width: number = 0; + protected height: number = 0; + + resize(width: number, height: number): this { + this.width = width; + this.height = height; + this.element.style.width = `${width}px`; + this.element.style.height = `${height}px`; + return this; + } +} diff --git a/src/new/core/gui/TabContainer.css b/src/new/core/gui/TabContainer.css new file mode 100644 index 00000000..cce87e05 --- /dev/null +++ b/src/new/core/gui/TabContainer.css @@ -0,0 +1,25 @@ +.core_TabContainer_Bar { + box-sizing: border-box; + background-color: hsl(0, 0%, 16%); + padding: 3px 0 0 0; +} + +.core_TabContainer_Tab { + display: inline-block; + height: 100%; + line-height: 25px; + padding: 0 10px; + margin: 0 1px; + color: #c0c0c0; + font-size: 15px; +} + +.core_TabContainer_Tab:hover { + background-color: hsl(0, 0%, 18%); + color: hsl(0, 0%, 85%); +} + +.core_TabContainer_Tab.active { + background-color: var(--bg-color); + color: hsl(0, 0%, 90%); +} diff --git a/src/new/core/gui/TabContainer.ts b/src/new/core/gui/TabContainer.ts new file mode 100644 index 00000000..1ea0e02f --- /dev/null +++ b/src/new/core/gui/TabContainer.ts @@ -0,0 +1,94 @@ +import { View } from "./View"; +import { create_el } from "./dom"; +import { LazyView } from "./LazyView"; +import { Resizable } from "./Resizable"; +import { ResizableView } from "./ResizableView"; +import "./TabContainer.css"; + +export type Tab = { + title: string; + key: string; + create_view: () => Promise; +}; + +type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; + +const BAR_HEIGHT = 28; + +export class TabContainer extends ResizableView { + element = create_el("div", "core_TabContainer"); + + private tabs: TabInfo[] = []; + private bar_element = create_el("div", "core_TabContainer_Bar"); + private panes_element = create_el("div", "core_TabContainer_Panes"); + + constructor(...tabs: Tab[]) { + super(); + + this.bar_element.onclick = this.bar_click; + + for (const tab of tabs) { + const tab_element = create_el("span", "core_TabContainer_Tab", tab_element => { + tab_element.textContent = tab.title; + tab_element.dataset["key"] = tab.key; + }); + this.bar_element.append(tab_element); + + const lazy_view = new LazyView(tab.create_view); + + this.tabs.push({ + ...tab, + tab_element, + lazy_view, + }); + + this.panes_element.append(lazy_view.element); + this.disposable(lazy_view); + } + + if (this.tabs.length) { + this.activate(this.tabs[0].key); + } + + this.element.append(this.bar_element, this.panes_element); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + this.bar_element.style.width = `${width}px`; + this.bar_element.style.height = `${BAR_HEIGHT}px`; + + const tab_pane_height = height - BAR_HEIGHT; + + this.panes_element.style.width = `${width}px`; + this.panes_element.style.height = `${tab_pane_height}px`; + + for (const tabs of this.tabs) { + tabs.lazy_view.resize(width, tab_pane_height); + } + + return this; + } + + private bar_click = (e: MouseEvent) => { + if (e.target instanceof HTMLElement) { + const key = e.target.dataset["key"]; + if (key) this.activate(key); + } + }; + + private activate(key: string): void { + for (const tab of this.tabs) { + const active = tab.key === key; + + if (active) { + tab.tab_element.classList.add("active"); + } else { + tab.tab_element.classList.remove("active"); + } + + tab.lazy_view.visible = active; + } + } +} diff --git a/src/new/core/gui/ToolBar.css b/src/new/core/gui/ToolBar.css new file mode 100644 index 00000000..e0bf1c3f --- /dev/null +++ b/src/new/core/gui/ToolBar.css @@ -0,0 +1,25 @@ +.core_ToolBar { + box-sizing: border-box; + padding-top: 1px; + border-bottom: solid var(--border-color) 1px; +} + +.core_ToolBar > * { + margin: 2px; +} + +.core_ToolBar .core_Button { + background-color: transparent; + border-color: transparent; +} + +.core_ToolBar .core_Button:hover { + background-color: #404040; + border-color: #505050; +} + +.core_ToolBar .core_Button:active { + background-color: #383838; + border-color: #404040; + color: #d0d0d0; +} diff --git a/src/new/core/gui/ToolBar.ts b/src/new/core/gui/ToolBar.ts new file mode 100644 index 00000000..34fa3bdb --- /dev/null +++ b/src/new/core/gui/ToolBar.ts @@ -0,0 +1,19 @@ +import { View } from "./View"; +import { create_el } from "./dom"; +import "./ToolBar.css"; + +export class ToolBar extends View { + readonly element = create_el("div", "core_ToolBar"); + readonly height = 32; + + constructor(...children: View[]) { + super(); + + this.element.style.height = `${this.height}px`; + + for (const child of children) { + this.element.append(child.element); + this.disposable(child); + } + } +} diff --git a/src/new/core/gui/View.ts b/src/new/core/gui/View.ts new file mode 100644 index 00000000..a4466317 --- /dev/null +++ b/src/new/core/gui/View.ts @@ -0,0 +1,17 @@ +import { Disposable } from "./Disposable"; + +export abstract class View implements Disposable { + abstract readonly element: HTMLElement; + + private disposables: Disposable[] = []; + + protected disposable(disposable: T): T { + this.disposables.push(disposable); + return disposable; + } + + dispose(): void { + this.element.remove(); + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/src/new/core/gui/dom.ts b/src/new/core/gui/dom.ts new file mode 100644 index 00000000..ba0ad91c --- /dev/null +++ b/src/new/core/gui/dom.ts @@ -0,0 +1,20 @@ +import { Disposable } from "./Disposable"; + +export function create_el( + tag_name: string, + class_name?: string, + modify?: (element: T) => void, +): T { + const element = document.createElement(tag_name) as T; + if (class_name) element.className = class_name; + if (modify) modify(element); + return element; +} + +export function disposable_el(element: HTMLElement): Disposable { + return { + dispose(): void { + element.remove(); + }, + }; +} diff --git a/src/new/core/observable/Observable.ts b/src/new/core/observable/Observable.ts new file mode 100644 index 00000000..3c6b9207 --- /dev/null +++ b/src/new/core/observable/Observable.ts @@ -0,0 +1,45 @@ +import { Disposable } from "../gui/Disposable"; + +export class Observable { + private value: T; + private readonly observers: ((new_value: T, old_value: T) => void)[] = []; + + constructor(value: T) { + this.value = value; + } + + get(): T { + return this.value; + } + + set(value: T): void { + if (value !== this.value) { + const old_value = this.value; + this.value = value; + + for (const observer of this.observers) { + try { + observer(value, old_value); + } catch (e) { + console.error(e); + } + } + } + } + + observe(observer: (new_value: T, old_value: T) => void): Disposable { + if (!this.observers.includes(observer)) { + this.observers.push(observer); + } + + return { + dispose: () => { + const index = this.observers.indexOf(observer); + + if (index !== -1) { + this.observers.splice(index, 1); + } + }, + }; + } +} diff --git a/src/new/core/stores/GuiStore.ts b/src/new/core/stores/GuiStore.ts new file mode 100644 index 00000000..9dc157d1 --- /dev/null +++ b/src/new/core/stores/GuiStore.ts @@ -0,0 +1,48 @@ +import { Observable } from "../observable/Observable"; + +export enum GuiTool { + Viewer, + QuestEditor, + HuntOptimizer, +} + +const GUI_TOOL_TO_STRING = new Map([ + [GuiTool.Viewer, "viewer"], + [GuiTool.QuestEditor, "quest_editor"], + [GuiTool.HuntOptimizer, "hunt_optimizer"], +]); +const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k])); + +class GuiStore { + tool_prop = new Observable(GuiTool.Viewer); + + get tool(): GuiTool { + return this.tool_prop.get(); + } + + set tool(tool: GuiTool) { + window.location.hash = `#/${gui_tool_to_string(tool)}`; + this.tool_prop.set(tool); + } + + constructor() { + const tool = window.location.hash.slice(2); + this.tool = string_to_gui_tool(tool) || GuiTool.Viewer; + } +} + +export const gui_store = new GuiStore(); + +function string_to_gui_tool(tool: string): GuiTool | undefined { + return STRING_TO_GUI_TOOL.get(tool); +} + +function gui_tool_to_string(tool: GuiTool): string { + const str = GUI_TOOL_TO_STRING.get(tool); + + if (str) { + return str; + } else { + throw new Error(`To string not implemented for ${(GuiTool as any)[tool]}.`); + } +} diff --git a/src/new/index.css b/src/new/index.css new file mode 100644 index 00000000..2174d9e1 --- /dev/null +++ b/src/new/index.css @@ -0,0 +1,42 @@ +:root { + --bg-color: hsl(0, 0%, 20%); + --text-color: hsl(0, 0%, 85%); + --border-color: hsl(0, 0%, 30%); + --scrollbar-color: hsl(0, 0%, 17%); + --scrollbar-thumb-color: hsl(0, 0%, 23%); +} + +* { + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-color); + + /* Turn off antd animations by turning all animations off. */ + animation-duration: 0s !important; + transition-duration: 0s !important; +} + +::-webkit-scrollbar { + background-color: var(--scrollbar-color); +} + +::-webkit-scrollbar-track { + background-color: var(--scrollbar-color); +} + +::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-color); +} + +::-webkit-scrollbar-corner { + background-color: var(--scrollbar-color); +} + +body { + cursor: default; + user-select: none; + overflow: hidden; + margin: 0; + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, + Arial, sans-serif; + background-color: var(--bg-color); + color: var(--text-color); +} diff --git a/src/new/index.ts b/src/new/index.ts new file mode 100644 index 00000000..269877fe --- /dev/null +++ b/src/new/index.ts @@ -0,0 +1,27 @@ +import { ApplicationView } from "./application/gui/ApplicationView"; +import { Disposable } from "./core/gui/Disposable"; +import "./index.css"; +import { throttle } from "lodash"; + +export function initialize(): Disposable { + const application_view = new ApplicationView(); + + const resize = throttle( + () => { + application_view.resize(window.innerWidth, window.innerHeight); + }, + 100, + { leading: true, trailing: true }, + ); + + resize(); + document.body.append(application_view.element); + window.addEventListener("resize", resize); + + return { + dispose(): void { + window.removeEventListener("resize", resize); + application_view.dispose(); + }, + }; +} diff --git a/src/new/viewer/domain/CharacterClassAnimation.ts b/src/new/viewer/domain/CharacterClassAnimation.ts new file mode 100644 index 00000000..30d940b9 --- /dev/null +++ b/src/new/viewer/domain/CharacterClassAnimation.ts @@ -0,0 +1,3 @@ +export class CharacterClassAnimation { + constructor(readonly id: number, readonly name: string) {} +} diff --git a/src/new/viewer/domain/CharacterClassModel.ts b/src/new/viewer/domain/CharacterClassModel.ts new file mode 100644 index 00000000..57c0a76b --- /dev/null +++ b/src/new/viewer/domain/CharacterClassModel.ts @@ -0,0 +1,8 @@ +export class CharacterClassModel { + constructor( + readonly name: string, + readonly head_style_count: number, + readonly hair_styles_count: number, + readonly hair_styles_with_accessory: Set, + ) {} +} diff --git a/src/new/viewer/gui/ModelView.css b/src/new/viewer/gui/ModelView.css new file mode 100644 index 00000000..39644d55 --- /dev/null +++ b/src/new/viewer/gui/ModelView.css @@ -0,0 +1,26 @@ +.viewer_ModelView_container { + display: flex; + flex-direction: row; +} + +.viewer_ModelSelectListView { + box-sizing: border-box; + list-style: none; + padding: 0; + margin: 0; + overflow: auto; +} + +.viewer_ModelSelectListView li { + padding: 4px 8px; +} + +.viewer_ModelSelectListView li:hover { + color: hsl(200, 25%, 85%); + background-color: hsl(0, 0%, 25%); +} + +.viewer_ModelSelectListView li.active { + color: hsl(200, 50%, 85%); + background-color: hsl(0, 0%, 30%); +} diff --git a/src/new/viewer/gui/ModelView.ts b/src/new/viewer/gui/ModelView.ts new file mode 100644 index 00000000..51b202b0 --- /dev/null +++ b/src/new/viewer/gui/ModelView.ts @@ -0,0 +1,123 @@ +import { create_el } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; +import { ToolBar } from "../../core/gui/ToolBar"; +import { Button } from "../../core/gui/Button"; +import "./ModelView.css"; +import { model_store } from "../stores/ModelStore"; +import { Observable } from "../../core/observable/Observable"; +import { RendererView } from "../../core/gui/RendererView"; +import { ModelRenderer } from "../rendering/ModelRenderer"; + +const MODEL_LIST_WIDTH = 100; +const ANIMATION_LIST_WIDTH = 150; + +export class ModelView extends ResizableView { + element = create_el("div", "viewer_ModelView"); + + private tool_bar = this.disposable(new ToolBar(new Button("Open file..."))); + + private container_element = create_el("div", "viewer_ModelView_container"); + private model_list_view = this.disposable( + new ModelSelectListView(model_store.models, model_store.current_model), + ); + private animation_list_view = this.disposable( + new ModelSelectListView(model_store.animations, model_store.current_animation), + ); + private renderer_view = this.disposable(new RendererView(new ModelRenderer())); + + constructor() { + super(); + + this.animation_list_view.borders = true; + + this.container_element.append( + this.model_list_view.element, + this.animation_list_view.element, + this.renderer_view.element, + ); + + this.element.append(this.tool_bar.element, this.container_element); + + model_store.current_model.set(model_store.models[5]); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + const container_height = Math.max(0, height - this.tool_bar.height); + + this.model_list_view.resize(MODEL_LIST_WIDTH, container_height); + this.animation_list_view.resize(ANIMATION_LIST_WIDTH, container_height); + this.renderer_view.resize( + Math.max(0, width - MODEL_LIST_WIDTH - ANIMATION_LIST_WIDTH), + container_height, + ); + + return this; + } +} + +class ModelSelectListView extends ResizableView { + element = create_el("ul", "viewer_ModelSelectListView"); + + set borders(borders: boolean) { + if (borders) { + this.element.style.borderLeft = "solid 1px var(--border-color)"; + this.element.style.borderRight = "solid 1px var(--border-color)"; + } else { + this.element.style.borderLeft = "none"; + this.element.style.borderRight = "none"; + } + } + + private selected_model?: T; + private selected_element?: HTMLLIElement; + + constructor(private models: T[], private selected: Observable) { + super(); + + this.element.onclick = this.list_click; + + models.forEach((model, index) => { + this.element.append( + create_el("li", undefined, li => { + li.textContent = model.name; + li.dataset["index"] = index.toString(); + }), + ); + }); + + this.disposable( + selected.observe(model => { + if (this.selected_element) { + this.selected_element.classList.remove("active"); + this.selected_element = undefined; + } + + if (model && model !== this.selected_model) { + const index = this.models.indexOf(model); + + if (index !== -1) { + this.selected_element = this.element.childNodes[index] as HTMLLIElement; + this.selected_element.classList.add("active"); + } + } + }), + ); + } + + private list_click = (e: MouseEvent) => { + if (e.target instanceof HTMLLIElement && e.target.dataset["index"]) { + if (this.selected_element) { + this.selected_element.classList.remove("active"); + } + + e.target.classList.add("active"); + + const index = parseInt(e.target.dataset["index"]!, 10); + + this.selected_element = e.target; + this.selected.set(this.models[index]); + } + }; +} diff --git a/src/new/viewer/gui/TextureView.ts b/src/new/viewer/gui/TextureView.ts new file mode 100644 index 00000000..d1fcdd9d --- /dev/null +++ b/src/new/viewer/gui/TextureView.ts @@ -0,0 +1,6 @@ +import { create_el } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; + +export class TextureView extends ResizableView { + element = create_el("div", "viewer_TextureView", el => (el.textContent = "Texture")); +} diff --git a/src/new/viewer/gui/ViewerView.ts b/src/new/viewer/gui/ViewerView.ts new file mode 100644 index 00000000..2a7efba8 --- /dev/null +++ b/src/new/viewer/gui/ViewerView.ts @@ -0,0 +1,29 @@ +import { TabContainer } from "../../core/gui/TabContainer"; +import { ResizableView } from "../../core/gui/ResizableView"; + +export class ViewerView extends ResizableView { + private tabs = this.disposable( + new TabContainer( + { + title: "Models", + key: "model", + create_view: async () => new (await import("./ModelView")).ModelView(), + }, + { + title: "Textures", + key: "texture", + create_view: async () => new (await import("./TextureView")).TextureView(), + }, + ), + ); + + get element(): HTMLElement { + return this.tabs.element; + } + + resize(width: number, height: number): this { + super.resize(width, height); + this.tabs.resize(width, height); + return this; + } +} diff --git a/src/new/viewer/rendering/ModelRenderer.ts b/src/new/viewer/rendering/ModelRenderer.ts new file mode 100644 index 00000000..fc67a480 --- /dev/null +++ b/src/new/viewer/rendering/ModelRenderer.ts @@ -0,0 +1,143 @@ +import { + DoubleSide, + Mesh, + MeshLambertMaterial, + Object3D, + PerspectiveCamera, + SkeletonHelper, + Texture, + Vector3, +} from "three"; +import { Renderer } from "../../../core/rendering/Renderer"; +import { model_store } from "../stores/ModelStore"; +import { Disposable } from "../../core/gui/Disposable"; +import { create_mesh, create_skinned_mesh } from "../../../core/rendering/conversion/create_mesh"; +import { ninja_object_to_buffer_geometry } from "../../../core/rendering/conversion/ninja_geometry"; +import { NjObject } from "../../../core/data_formats/parsing/ninja"; + +export class ModelRenderer extends Renderer implements Disposable { + private nj_object?: NjObject; + private object_3d?: Object3D; + private skeleton_helper?: SkeletonHelper; + private perspective_camera: PerspectiveCamera; + private disposables: Disposable[] = []; + + constructor() { + super(new PerspectiveCamera(75, 1, 1, 200)); + + this.perspective_camera = this.camera as PerspectiveCamera; + + this.disposables.push(model_store.current_nj_data.observe(this.update)); + } + + set_size(width: number, height: number): void { + this.perspective_camera.aspect = width / height; + this.perspective_camera.updateProjectionMatrix(); + super.set_size(width, height); + } + + dispose(): void { + super.dispose(); + this.disposables.forEach(d => d.dispose()); + } + + protected render(): void { + // if (model_viewer_store.animation) { + // model_viewer_store.animation.mixer.update(model_viewer_store.clock.getDelta()); + // model_viewer_store.update_animation_frame(); + // } + + this.light_holder.quaternion.copy(this.perspective_camera.quaternion); + super.render(); + + // if (model_viewer_store.animation && !model_viewer_store.animation.action.paused) { + // this.schedule_render(); + // } + } + + private update = () => { + // TODO: + const textures: Texture[] | undefined = Math.random() > 1 ? [] : undefined; + const nj_data = model_store.current_nj_data.get(); + + if (nj_data) { + const { nj_object, has_skeleton } = nj_data; + + if (nj_object !== this.nj_object) { + this.nj_object = nj_object; + + if (nj_object) { + let mesh: Mesh; + + const materials = + textures && + textures.map( + tex => + new MeshLambertMaterial({ + skinning: has_skeleton, + map: tex, + side: DoubleSide, + alphaTest: 0.5, + }), + ); + + if (has_skeleton) { + mesh = create_skinned_mesh( + ninja_object_to_buffer_geometry(nj_object), + materials, + ); + } else { + mesh = create_mesh(ninja_object_to_buffer_geometry(nj_object), materials); + } + + // Make sure we rotate around the center of the model instead of its origin. + const bb = mesh.geometry.boundingBox; + const height = bb.max.y - bb.min.y; + mesh.translateY(-height / 2 - bb.min.y); + + this.set_object_3d(mesh); + } else { + this.set_object_3d(undefined); + } + } + + if (this.skeleton_helper) { + this.skeleton_helper.visible = model_store.show_skeleton.get(); + } + + // if (model_viewer_store.animation) { + // this.schedule_render(); + // } + // + // if (!model_viewer_store.animation_playing) { + // // Reference animation_frame here to make sure we render when the user sets the frame manually. + // model_viewer_store.animation_frame; + // this.schedule_render(); + // } + } else { + this.set_object_3d(undefined); + } + + this.schedule_render(); + }; + + private set_object_3d(object_3d?: Object3D): void { + if (this.object_3d) { + this.scene.remove(this.object_3d); + this.scene.remove(this.skeleton_helper!); + this.skeleton_helper = undefined; + } + + if (object_3d) { + this.scene.add(object_3d); + this.skeleton_helper = new SkeletonHelper(object_3d); + this.skeleton_helper.visible = model_store.show_skeleton.get(); + (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.object_3d = object_3d; + this.schedule_render(); + } +} diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts new file mode 100644 index 00000000..4f4bdba7 --- /dev/null +++ b/src/new/viewer/stores/ModelStore.ts @@ -0,0 +1,286 @@ +import { Clock } from "three"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { NjMotion } from "../../../core/data_formats/parsing/ninja/motion"; +import { NjObject, parse_nj } from "../../../core/data_formats/parsing/ninja"; +import { CharacterClassModel } from "../domain/CharacterClassModel"; +import { CharacterClassAnimation } from "../domain/CharacterClassAnimation"; +import { Observable } from "../../core/observable/Observable"; +import { get_player_data } from "../../../viewer/loading/player"; +import { Disposable } from "../../core/gui/Disposable"; + +const nj_object_cache: Map> = new Map(); +const nj_motion_cache: Map> = new Map(); + +// TODO: move all Three.js stuff into the renderer. +class ModelStore implements Disposable { + readonly models: CharacterClassModel[] = [ + new CharacterClassModel("HUmar", 1, 10, new Set([6])), + new CharacterClassModel("HUnewearl", 1, 10, new Set()), + new CharacterClassModel("HUcast", 5, 0, new Set()), + new CharacterClassModel("HUcaseal", 5, 0, new Set()), + new CharacterClassModel("RAmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + new CharacterClassModel("RAmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + new CharacterClassModel("RAcast", 5, 0, new Set()), + new CharacterClassModel("RAcaseal", 5, 0, new Set()), + new CharacterClassModel("FOmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + new CharacterClassModel("FOmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + new CharacterClassModel("FOnewm", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + new CharacterClassModel("FOnewearl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + ]; + + readonly animations: CharacterClassAnimation[] = new Array(572) + .fill(undefined) + .map((_, i) => new CharacterClassAnimation(i, `Animation ${i + 1}`)); + + readonly clock = new Clock(); + + readonly current_model = new Observable(undefined); + + readonly current_nj_data = new Observable< + | { + nj_object: NjObject; + bone_count: number; + has_skeleton: boolean; + } + | undefined + >(undefined); + + readonly current_animation = new Observable(undefined); + + // @observable.ref animation?: { + // player_animation?: CharacterClassAnimation; + // mixer: AnimationMixer; + // clip: AnimationClip; + // action: AnimationAction; + // }; + // @observable animation_playing: boolean = false; + // @observable animation_frame_rate: number = PSO_FRAME_RATE; + // @observable animation_frame: number = 0; + // @observable animation_frame_count: number = 0; + + readonly show_skeleton = new Observable(false); + + private disposables: Disposable[] = []; + + constructor() { + this.disposables.push(this.current_model.observe(this.load_model)); + } + + dispose(): void { + this.disposables.forEach(d => d.dispose()); + } + + // set_animation_frame_rate = (rate: number) => { + // if (this.animation) { + // this.animation.mixer.timeScale = rate / PSO_FRAME_RATE; + // this.animation_frame_rate = rate; + // } + // }; + // + // set_animation_frame = (frame: number) => { + // if (this.animation) { + // const frame_count = this.animation_frame_count; + // if (frame > frame_count) frame = 1; + // if (frame < 1) frame = frame_count; + // this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; + // this.animation_frame = frame; + // } + // }; + + // load_animation = async (animation: CharacterClassAnimation) => { + // const nj_motion = await this.get_nj_motion(animation); + // const nj_data = this.current_nj_data.get(); + // + // if (nj_data) { + // this.set_animation(create_animation_clip(nj_data, nj_motion), animation); + // } + // }; + + // 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_selected(model, true); + // } else if (file.name.endsWith(".xj")) { + // const model = parse_xj(cursor)[0]; + // this.set_selected(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 if (file.name.endsWith(".xvm")) { + // if (this.current_model) { + // const xvm = parse_xvm(cursor); + // this.set_textures(xvm_to_textures(xvm)); + // } + // } else { + // logger.error(`Unknown file extension in filename "${file.name}".`); + // } + // } catch (e) { + // logger.error("Couldn't read file.", e); + // } + // }; + + // pause_animation = () => { + // if (this.animation) { + // this.animation.action.paused = true; + // this.animation_playing = false; + // this.clock.stop(); + // } + // }; + // + // toggle_animation_playing = () => { + // if (this.animation) { + // this.animation.action.paused = !this.animation.action.paused; + // this.animation_playing = !this.animation.action.paused; + // + // if (this.animation_playing) { + // this.clock.start(); + // } else { + // this.clock.stop(); + // } + // } + // }; + + // update_animation_frame = () => { + // if (this.animation && this.animation_playing) { + // const time = this.animation.action.time; + // this.animation_frame = Math.round(time * PSO_FRAME_RATE) + 1; + // } + // }; + + // set_animation = (clip: AnimationClip, animation?: CharacterClassAnimation) => { + // if (!this.current_obj3d || !(this.current_obj3d instanceof SkinnedMesh)) return; + // + // let mixer: AnimationMixer; + // + // if (this.animation) { + // this.animation.mixer.stopAllAction(); + // mixer = this.animation.mixer; + // } else { + // mixer = new AnimationMixer(this.current_obj3d); + // } + // + // this.animation = { + // player_animation: animation, + // mixer, + // clip, + // action: mixer.clipAction(clip), + // }; + // + // this.clock.start(); + // this.animation.action.play(); + // this.animation_playing = true; + // this.animation_frame_count = Math.round(PSO_FRAME_RATE * clip.duration) + 1; + // }; + + private load_model = async (model?: CharacterClassModel) => { + if (model) { + const nj_object = await this.get_player_nj_object(model); + // if (this.current_obj3d && this.animation) { + // this.animation.mixer.stopAllAction(); + // this.animation.mixer.uncacheRoot(this.current_obj3d); + // this.animation = undefined; + // } + + this.current_nj_data.set({ + nj_object, + // Ignore the bones from the head parts. + bone_count: model ? 64 : nj_object.bone_count(), + has_skeleton: true, + }); + } else { + this.current_nj_data.set(undefined); + } + }; + + private async get_player_nj_object(model: CharacterClassModel): Promise { + let nj_object = nj_object_cache.get(model.name); + + if (nj_object) { + return nj_object; + } else { + nj_object = this.get_all_assets(model); + nj_object_cache.set(model.name, nj_object); + return nj_object; + } + } + + private async get_all_assets(model: CharacterClassModel): Promise { + const body_data = await get_player_data(model.name, "Body"); + const body = parse_nj(new ArrayBufferCursor(body_data, Endianness.Little))[0]; + + if (!body) { + throw new Error(`Couldn't parse body for player class ${model.name}.`); + } + + const head_data = await get_player_data(model.name, "Head", 0); + const head = parse_nj(new ArrayBufferCursor(head_data, Endianness.Little))[0]; + + if (head) { + this.add_to_bone(body, head, 59); + } + + if (model.hair_styles_count > 0) { + const hair_data = await get_player_data(model.name, "Hair", 0); + const hair = parse_nj(new ArrayBufferCursor(hair_data, Endianness.Little))[0]; + + if (hair) { + this.add_to_bone(body, hair, 59); + } + + if (model.hair_styles_with_accessory.has(0)) { + const accessory_data = await get_player_data(model.name, "Accessory", 0); + const accessory = parse_nj( + new ArrayBufferCursor(accessory_data, Endianness.Little), + )[0]; + + if (accessory) { + this.add_to_bone(body, accessory, 59); + } + } + } + + return body; + } + + private add_to_bone(object: NjObject, head_part: NjObject, bone_id: number): void { + const bone = object.get_bone(bone_id); + + if (bone) { + bone.evaluation_flags.hidden = false; + bone.evaluation_flags.break_child_trace = false; + bone.children.push(head_part); + } + } + + // private async get_nj_motion(animation: CharacterClassAnimation): Promise { + // let nj_motion = nj_motion_cache.get(animation.id); + // + // if (nj_motion) { + // return nj_motion; + // } else { + // nj_motion = get_player_animation_data(animation.id).then(motion_data => + // parse_njm( + // new ArrayBufferCursor(motion_data, Endianness.Little), + // this.current_bone_count, + // ), + // ); + // + // nj_motion_cache.set(animation.id, nj_motion); + // return nj_motion; + // } + // } + // + // private set_textures = (textures: Texture[]) => { + // this.set_obj3d(textures); + // }; +} + +export const model_store = new ModelStore(); diff --git a/src/quest_editor/rendering/QuestRenderer.ts b/src/quest_editor/rendering/QuestRenderer.ts index 42ba8015..31bb1b34 100644 --- a/src/quest_editor/rendering/QuestRenderer.ts +++ b/src/quest_editor/rendering/QuestRenderer.ts @@ -1,13 +1,11 @@ import { autorun } from "mobx"; -import { Mesh, Object3D, PerspectiveCamera, Group } from "three"; +import { Group, Mesh, Object3D, PerspectiveCamera } from "three"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { QuestEntityControls } from "./QuestEntityControls"; import { QuestModelManager } from "./QuestModelManager"; import { Renderer } from "../../core/rendering/Renderer"; import { EntityUserData } from "./conversion/entities"; import { ObservableQuestEntity } from "../domain/observable_quest_entities"; -import { DND_OBJECT_TYPE } from "../ui/UiConstants"; -import { DragEvent } from "react"; let renderer: QuestRenderer | undefined; @@ -16,7 +14,7 @@ export function get_quest_renderer(): QuestRenderer { return renderer; } -export class QuestRenderer extends Renderer { +export class QuestRenderer extends Renderer { get debug(): boolean { return this._debug; } @@ -60,12 +58,15 @@ export class QuestRenderer extends Renderer { return this._entity_models; } + private perspective_camera: PerspectiveCamera; private entity_to_mesh = new Map(); private entity_controls: QuestEntityControls; constructor() { super(new PerspectiveCamera(60, 1, 10, 10000)); + this.perspective_camera = this.camera as PerspectiveCamera; + const model_manager = new QuestModelManager(this); autorun( @@ -86,8 +87,8 @@ export class QuestRenderer extends Renderer { } set_size(width: number, height: number): void { - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); + this.perspective_camera.aspect = width / height; + this.perspective_camera.updateProjectionMatrix(); super.set_size(width, height); } diff --git a/src/viewer/loading/player.ts b/src/viewer/loading/player.ts index 1dfe6837..b5e5969d 100644 --- a/src/viewer/loading/player.ts +++ b/src/viewer/loading/player.ts @@ -15,5 +15,5 @@ export async function get_player_animation_data(animation_id: number): Promise { +export class ModelRenderer extends Renderer { private model?: Object3D; private skeleton_helper?: SkeletonHelper; + private perspective_camera: PerspectiveCamera; constructor() { super(new PerspectiveCamera(75, 1, 1, 200)); + this.perspective_camera = this.camera as PerspectiveCamera; + autorun(() => { this.set_model(model_viewer_store.current_obj3d); @@ -40,8 +43,8 @@ export class ModelRenderer extends Renderer { } set_size(width: number, height: number): void { - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); + this.perspective_camera.aspect = width / height; + this.perspective_camera.updateProjectionMatrix(); super.set_size(width, height); } @@ -51,7 +54,7 @@ export class ModelRenderer extends Renderer { model_viewer_store.update_animation_frame(); } - this.light_holder.quaternion.copy(this.camera.quaternion); + this.light_holder.quaternion.copy(this.perspective_camera.quaternion); super.render(); if (model_viewer_store.animation && !model_viewer_store.animation.action.paused) { diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 0c9afdeb..09add470 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -23,12 +23,15 @@ export function get_texture_renderer(): TextureRenderer { return renderer; } -export class TextureRenderer extends Renderer { +export class TextureRenderer extends Renderer { + private ortho_camera: OrthographicCamera; private quad_meshes: Mesh[] = []; constructor() { super(new OrthographicCamera(-400, 400, 300, -300, 1, 10)); + this.ortho_camera = this.camera as OrthographicCamera; + this.controls.azimuthRotateSpeed = 0; this.controls.polarRotateSpeed = 0; @@ -47,11 +50,11 @@ export class TextureRenderer extends Renderer { } 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(); + this.ortho_camera.left = -Math.floor(width / 2); + this.ortho_camera.right = Math.ceil(width / 2); + this.ortho_camera.top = Math.floor(height / 2); + this.ortho_camera.bottom = -Math.ceil(height / 2); + this.ortho_camera.updateProjectionMatrix(); super.set_size(width, height); } diff --git a/tsconfig.json b/tsconfig.json index 546e127e..b2b12070 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, - "module": "es6", + "module": "commonjs", "target": "es6", "lib": ["es6", "dom", "dom.iterable"], "allowJs": true, diff --git a/webpack.dev.js b/webpack.dev.js index 72172784..626d1515 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -38,9 +38,9 @@ module.exports = merge(common, { loader: "css-loader", options: { sourceMap: true, - modules: { - localIdentName: "[path][name]__[local]", - }, + // modules: { + // localIdentName: "[path][name]__[local]", + // }, }, }, ],