From 5571f6b1a83af586a2a82610a37ffa8e8c9ea216 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Mon, 19 Aug 2019 22:56:40 +0200 Subject: [PATCH 01/50] 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]", + // }, }, }, ], From c7cbf684113500f26fc3b704d0da07bebd255a90 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Mon, 19 Aug 2019 23:49:40 +0200 Subject: [PATCH 02/50] The model viewer can load NJ and XJ files again. --- src/new/core/gui/Button.css | 1 + src/new/core/gui/Button.ts | 6 +++ src/new/core/gui/FileInput.css | 9 ++++ src/new/core/gui/FileInput.ts | 29 +++++++++++ src/new/core/gui/View.ts | 1 + src/new/viewer/gui/ModelView.ts | 27 ++++++++-- src/new/viewer/stores/ModelStore.ts | 80 ++++++++++++++++++----------- 7 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 src/new/core/gui/FileInput.css create mode 100644 src/new/core/gui/FileInput.ts diff --git a/src/new/core/gui/Button.css b/src/new/core/gui/Button.css index 4c496445..0c9e08ce 100644 --- a/src/new/core/gui/Button.css +++ b/src/new/core/gui/Button.css @@ -1,4 +1,5 @@ .core_Button { + display: inline-block; box-sizing: border-box; background-color: #404040; height: 26px; diff --git a/src/new/core/gui/Button.ts b/src/new/core/gui/Button.ts index 35af9635..9e1fbabc 100644 --- a/src/new/core/gui/Button.ts +++ b/src/new/core/gui/Button.ts @@ -2,6 +2,8 @@ import { create_el } from "./dom"; import { View } from "./View"; import "./Button.css"; +function dummy_function(): void {} + export class Button extends View { element: HTMLButtonElement = create_el("button", "core_Button"); @@ -9,5 +11,9 @@ export class Button extends View { super(); this.element.textContent = text; + + this.element.onclick = () => this.on_click(); } + + on_click: () => void = dummy_function; } diff --git a/src/new/core/gui/FileInput.css b/src/new/core/gui/FileInput.css new file mode 100644 index 00000000..5418a1db --- /dev/null +++ b/src/new/core/gui/FileInput.css @@ -0,0 +1,9 @@ +.core_FileInput_input { + overflow: hidden; + clip: rect(0, 0, 0, 0); + position: absolute; + width: 1px; + height: 1px; + border: none; + padding: 0; +} diff --git a/src/new/core/gui/FileInput.ts b/src/new/core/gui/FileInput.ts new file mode 100644 index 00000000..9cef9f29 --- /dev/null +++ b/src/new/core/gui/FileInput.ts @@ -0,0 +1,29 @@ +import { create_el } from "./dom"; +import { View } from "./View"; +import "./FileInput.css"; +import "./Button.css"; + +function dummy_function(): void {} + +export class FileInput extends View { + private input: HTMLInputElement = create_el("input", "core_FileInput_input"); + + element: HTMLLabelElement = create_el("label", "core_Button"); + + constructor(text: string, accept: string = "") { + super(); + + this.input.type = "file"; + this.input.accept = accept; + this.input.onchange = () => { + if (this.input.files && this.input.files.length) { + this.on_files_chosen([...this.input.files!]); + } + }; + + this.element.textContent = text; + this.element.append(this.input); + } + + on_files_chosen: (files: File[]) => void = dummy_function; +} diff --git a/src/new/core/gui/View.ts b/src/new/core/gui/View.ts index a4466317..af952a85 100644 --- a/src/new/core/gui/View.ts +++ b/src/new/core/gui/View.ts @@ -13,5 +13,6 @@ export abstract class View implements Disposable { dispose(): void { this.element.remove(); this.disposables.forEach(d => d.dispose()); + this.disposables.splice(0, this.disposables.length); } } diff --git a/src/new/viewer/gui/ModelView.ts b/src/new/viewer/gui/ModelView.ts index 51b202b0..6a592643 100644 --- a/src/new/viewer/gui/ModelView.ts +++ b/src/new/viewer/gui/ModelView.ts @@ -1,12 +1,13 @@ 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"; +import { View } from "../../core/gui/View"; +import { FileInput } from "../../core/gui/FileInput"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 150; @@ -14,8 +15,7 @@ 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 tool_bar_view = this.disposable(new ToolBarView()); private container_element = create_el("div", "viewer_ModelView_container"); private model_list_view = this.disposable( new ModelSelectListView(model_store.models, model_store.current_model), @@ -36,7 +36,7 @@ export class ModelView extends ResizableView { this.renderer_view.element, ); - this.element.append(this.tool_bar.element, this.container_element); + this.element.append(this.tool_bar_view.element, this.container_element); model_store.current_model.set(model_store.models[5]); } @@ -44,7 +44,7 @@ export class ModelView extends ResizableView { resize(width: number, height: number): this { super.resize(width, height); - const container_height = Math.max(0, height - this.tool_bar.height); + const container_height = Math.max(0, height - this.tool_bar_view.height); this.model_list_view.resize(MODEL_LIST_WIDTH, container_height); this.animation_list_view.resize(ANIMATION_LIST_WIDTH, container_height); @@ -57,6 +57,23 @@ export class ModelView extends ResizableView { } } +class ToolBarView extends View { + private readonly open_file_button = new FileInput("Open file...", ".nj, .xj"); + private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); + + readonly element = this.tool_bar.element; + + get height(): number { + return this.tool_bar.height; + } + + constructor() { + super(); + + this.open_file_button.on_files_chosen = files => model_store.load_file(files[0]); + } +} + class ModelSelectListView extends ResizableView { element = create_el("ul", "viewer_ModelSelectListView"); diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts index 4f4bdba7..544c5da3 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/new/viewer/stores/ModelStore.ts @@ -1,14 +1,20 @@ 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 { NjMotion, parse_njm } from "../../../core/data_formats/parsing/ninja/motion"; +import { NjObject, parse_nj, parse_xj } 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"; +import { read_file } from "../../../core/read_file"; +import { create_animation_clip } from "../../../core/rendering/conversion/ninja_animation"; +import { parse_xvm } from "../../../core/data_formats/parsing/ninja/texture"; +import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; +import Logger = require("js-logger"); +const logger = Logger.get("viewer/stores/ModelStore"); const nj_object_cache: Map> = new Map(); const nj_motion_cache: Map> = new Map(); @@ -98,34 +104,48 @@ class ModelStore implements Disposable { // }; // 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); - // } - // }; + load_file = async (file: File) => { + try { + this.current_model.set(undefined); + this.current_nj_data.set(undefined); + this.current_animation.set(undefined); + + const buffer = await read_file(file); + const cursor = new ArrayBufferCursor(buffer, Endianness.Little); + + if (file.name.endsWith(".nj")) { + const nj_object = parse_nj(cursor)[0]; + + this.current_nj_data.set({ + nj_object, + bone_count: nj_object.bone_count(), + has_skeleton: true, + }); + } else if (file.name.endsWith(".xj")) { + const nj_object = parse_xj(cursor)[0]; + + this.current_nj_data.set({ + nj_object, + bone_count: 0, + has_skeleton: 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) { From 3ba13606aae7cac63b5a24cf1cdd3e9460d9becd Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Tue, 20 Aug 2019 15:02:58 +0200 Subject: [PATCH 03/50] More work on new UI. --- .../rendering/conversion/ninja_animation.ts | 7 +- src/new/application/gui/MainContentView.ts | 10 +- src/new/application/gui/NavigationView.ts | 6 +- src/new/core/gui/Button.ts | 7 +- src/new/core/gui/CheckBox.css | 6 + src/new/core/gui/CheckBox.ts | 21 ++ src/new/core/gui/FileInput.ts | 11 +- src/new/core/gui/ToolBar.css | 2 + src/new/core/observable/Observable.ts | 32 +-- src/new/core/observable/Property.ts | 22 ++ src/new/core/stores/GuiStore.ts | 22 +- src/new/viewer/gui/ModelView.ts | 25 ++- src/new/viewer/rendering/ModelRenderer.ts | 193 +++++++++++------- src/new/viewer/stores/ModelStore.ts | 158 ++++++-------- .../loading/{player.ts => character_class.ts} | 8 +- src/viewer/stores/ModelViewerStore.ts | 12 +- webpack.dev.js | 25 --- webpack.prod.js | 34 +-- 18 files changed, 307 insertions(+), 294 deletions(-) create mode 100644 src/new/core/gui/CheckBox.css create mode 100644 src/new/core/gui/CheckBox.ts create mode 100644 src/new/core/observable/Property.ts rename src/viewer/loading/{player.ts => character_class.ts} (52%) diff --git a/src/core/rendering/conversion/ninja_animation.ts b/src/core/rendering/conversion/ninja_animation.ts index dded2114..4c2767e1 100644 --- a/src/core/rendering/conversion/ninja_animation.ts +++ b/src/core/rendering/conversion/ninja_animation.ts @@ -8,7 +8,7 @@ import { QuaternionKeyframeTrack, VectorKeyframeTrack, } from "three"; -import { NjModel, NjObject } from "../../data_formats/parsing/ninja"; +import { NjObject } from "../../data_formats/parsing/ninja"; import { NjInterpolation, NjKeyframeTrackType, @@ -17,10 +17,7 @@ import { export const PSO_FRAME_RATE = 30; -export function create_animation_clip( - nj_object: NjObject, - nj_motion: NjMotion, -): AnimationClip { +export function create_animation_clip(nj_object: NjObject, nj_motion: NjMotion): AnimationClip { const interpolation = nj_motion.interpolation === NjInterpolation.Spline ? InterpolateSmooth : InterpolateLinear; diff --git a/src/new/application/gui/MainContentView.ts b/src/new/application/gui/MainContentView.ts index c0acde22..48e6dc79 100644 --- a/src/new/application/gui/MainContentView.ts +++ b/src/new/application/gui/MainContentView.ts @@ -23,8 +23,10 @@ export class MainContentView extends ResizableView { 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)); + const tool_view = this.tool_views.get(gui_store.tool.get()); + if (tool_view) tool_view.visible = true; + + this.disposable(gui_store.tool.observe(this.tool_changed)); } resize(width: number, height: number): this { @@ -37,8 +39,8 @@ export class MainContentView extends ResizableView { return this; } - private tool_changed = (new_tool: GuiTool, old_tool: GuiTool) => { - const old_view = this.tool_views.get(old_tool); + private tool_changed = (new_tool: GuiTool, { old_value }: { old_value: GuiTool }) => { + const old_view = this.tool_views.get(old_value); if (old_view) old_view.visible = false; const new_view = this.tool_views.get(new_tool); diff --git a/src/new/application/gui/NavigationView.ts b/src/new/application/gui/NavigationView.ts index 15ac1f4c..25f5814a 100644 --- a/src/new/application/gui/NavigationView.ts +++ b/src/new/application/gui/NavigationView.ts @@ -27,13 +27,13 @@ export class NavigationView extends View { this.element.append(button.element); } - this.tool_changed(gui_store.tool); - this.disposable(gui_store.tool_prop.observe(this.tool_changed)); + this.tool_changed(gui_store.tool.get()); + this.disposable(gui_store.tool.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]; + gui_store.tool.set((GuiTool as any)[e.target.control.value]); } } diff --git a/src/new/core/gui/Button.ts b/src/new/core/gui/Button.ts index 9e1fbabc..525d8b71 100644 --- a/src/new/core/gui/Button.ts +++ b/src/new/core/gui/Button.ts @@ -1,8 +1,7 @@ import { create_el } from "./dom"; import { View } from "./View"; import "./Button.css"; - -function dummy_function(): void {} +import { Observable } from "../observable/Observable"; export class Button extends View { element: HTMLButtonElement = create_el("button", "core_Button"); @@ -12,8 +11,8 @@ export class Button extends View { this.element.textContent = text; - this.element.onclick = () => this.on_click(); + this.element.onclick = (e: MouseEvent) => this.click.fire(e, undefined); } - on_click: () => void = dummy_function; + click = new Observable(); } diff --git a/src/new/core/gui/CheckBox.css b/src/new/core/gui/CheckBox.css new file mode 100644 index 00000000..53379309 --- /dev/null +++ b/src/new/core/gui/CheckBox.css @@ -0,0 +1,6 @@ +.core_CheckBox { + display: inline-flex; + flex-direction: row; + height: 26px; + align-items: center; +} diff --git a/src/new/core/gui/CheckBox.ts b/src/new/core/gui/CheckBox.ts new file mode 100644 index 00000000..5f485234 --- /dev/null +++ b/src/new/core/gui/CheckBox.ts @@ -0,0 +1,21 @@ +import { View } from "./View"; +import { create_el } from "./dom"; +import "./CheckBox.css"; +import { Property } from "../observable/Property"; + +export class CheckBox extends View { + private input: HTMLInputElement = create_el("input"); + + element: HTMLLabelElement = create_el("label", "core_CheckBox"); + + constructor(text: string) { + super(); + + this.input.type = "checkbox"; + this.input.onchange = () => this.checked.set(this.input.checked); + + this.element.append(this.input, text); + } + + checked = new Property(false); +} diff --git a/src/new/core/gui/FileInput.ts b/src/new/core/gui/FileInput.ts index 9cef9f29..6b2e82d1 100644 --- a/src/new/core/gui/FileInput.ts +++ b/src/new/core/gui/FileInput.ts @@ -2,13 +2,12 @@ import { create_el } from "./dom"; import { View } from "./View"; import "./FileInput.css"; import "./Button.css"; - -function dummy_function(): void {} +import { Property } from "../observable/Property"; export class FileInput extends View { private input: HTMLInputElement = create_el("input", "core_FileInput_input"); - element: HTMLLabelElement = create_el("label", "core_Button"); + element: HTMLLabelElement = create_el("label", "core_FileInput core_Button"); constructor(text: string, accept: string = "") { super(); @@ -17,7 +16,9 @@ export class FileInput extends View { this.input.accept = accept; this.input.onchange = () => { if (this.input.files && this.input.files.length) { - this.on_files_chosen([...this.input.files!]); + this.files.set([...this.input.files!]); + } else { + this.files.set([]); } }; @@ -25,5 +26,5 @@ export class FileInput extends View { this.element.append(this.input); } - on_files_chosen: (files: File[]) => void = dummy_function; + readonly files = new Property([]); } diff --git a/src/new/core/gui/ToolBar.css b/src/new/core/gui/ToolBar.css index e0bf1c3f..349bd89a 100644 --- a/src/new/core/gui/ToolBar.css +++ b/src/new/core/gui/ToolBar.css @@ -1,5 +1,7 @@ .core_ToolBar { box-sizing: border-box; + display: flex; + flex-direction: row; padding-top: 1px; border-bottom: solid var(--border-color) 1px; } diff --git a/src/new/core/observable/Observable.ts b/src/new/core/observable/Observable.ts index 3c6b9207..1dc400e1 100644 --- a/src/new/core/observable/Observable.ts +++ b/src/new/core/observable/Observable.ts @@ -1,33 +1,19 @@ import { Disposable } from "../gui/Disposable"; -export class Observable { - private value: T; - private readonly observers: ((new_value: T, old_value: T) => void)[] = []; +export class Observable { + private readonly observers: ((event: E, meta: M) => 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); - } + fire(event: E, meta: M): void { + for (const observer of this.observers) { + try { + observer(event, meta); + } catch (e) { + console.error(e); } } } - observe(observer: (new_value: T, old_value: T) => void): Disposable { + observe(observer: (event: E, meta: M) => void): Disposable { if (!this.observers.includes(observer)) { this.observers.push(observer); } diff --git a/src/new/core/observable/Property.ts b/src/new/core/observable/Property.ts new file mode 100644 index 00000000..d613801a --- /dev/null +++ b/src/new/core/observable/Property.ts @@ -0,0 +1,22 @@ +import { Observable } from "./Observable"; + +export class Property extends Observable { + private value: T; + + constructor(value: T) { + super(); + this.value = value; + } + + get(): T { + return this.value; + } + + set(value: T): void { + if (value !== this.value) { + const old_value = this.value; + this.value = value; + this.fire(value, { old_value }); + } + } +} diff --git a/src/new/core/stores/GuiStore.ts b/src/new/core/stores/GuiStore.ts index 9dc157d1..2e46be63 100644 --- a/src/new/core/stores/GuiStore.ts +++ b/src/new/core/stores/GuiStore.ts @@ -1,4 +1,5 @@ -import { Observable } from "../observable/Observable"; +import { Property } from "../observable/Property"; +import { Disposable } from "../gui/Disposable"; export enum GuiTool { Viewer, @@ -13,21 +14,20 @@ const GUI_TOOL_TO_STRING = new Map([ ]); 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); +class GuiStore implements Disposable { + tool = new Property(GuiTool.Viewer); - get tool(): GuiTool { - return this.tool_prop.get(); - } - - set tool(tool: GuiTool) { + private hash_disposer = this.tool.observe(tool => { 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; + this.tool.set(string_to_gui_tool(tool) || GuiTool.Viewer); + } + + dispose(): void { + this.hash_disposer.dispose(); } } diff --git a/src/new/viewer/gui/ModelView.ts b/src/new/viewer/gui/ModelView.ts index 6a592643..e0f658b4 100644 --- a/src/new/viewer/gui/ModelView.ts +++ b/src/new/viewer/gui/ModelView.ts @@ -3,11 +3,12 @@ import { ResizableView } from "../../core/gui/ResizableView"; import { ToolBar } from "../../core/gui/ToolBar"; import "./ModelView.css"; import { model_store } from "../stores/ModelStore"; -import { Observable } from "../../core/observable/Observable"; +import { Property } from "../../core/observable/Property"; import { RendererView } from "../../core/gui/RendererView"; import { ModelRenderer } from "../rendering/ModelRenderer"; import { View } from "../../core/gui/View"; import { FileInput } from "../../core/gui/FileInput"; +import { CheckBox } from "../../core/gui/CheckBox"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 150; @@ -58,8 +59,12 @@ export class ModelView extends ResizableView { } class ToolBarView extends View { - private readonly open_file_button = new FileInput("Open file...", ".nj, .xj"); - private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); + private readonly open_file_button = new FileInput("Open file...", ".nj, .njm, .xj"); + private readonly skeleton_checkbox = new CheckBox("Show skeleton"); + + private readonly tool_bar = this.disposable( + new ToolBar(this.open_file_button, this.skeleton_checkbox), + ); readonly element = this.tool_bar.element; @@ -70,7 +75,17 @@ class ToolBarView extends View { constructor() { super(); - this.open_file_button.on_files_chosen = files => model_store.load_file(files[0]); + this.disposable( + this.open_file_button.files.observe(files => { + if (files.length) model_store.load_file(files[0]); + }), + ); + + this.disposable( + this.skeleton_checkbox.checked.observe(checked => + model_store.show_skeleton.set(checked), + ), + ); } } @@ -90,7 +105,7 @@ class ModelSelectListView extends ResizableView { private selected_model?: T; private selected_element?: HTMLLIElement; - constructor(private models: T[], private selected: Observable) { + constructor(private models: T[], private selected: Property) { super(); this.element.onclick = this.list_click; diff --git a/src/new/viewer/rendering/ModelRenderer.ts b/src/new/viewer/rendering/ModelRenderer.ts index fc67a480..21adcb5b 100644 --- a/src/new/viewer/rendering/ModelRenderer.ts +++ b/src/new/viewer/rendering/ModelRenderer.ts @@ -1,10 +1,15 @@ import { + AnimationAction, + AnimationClip, + AnimationMixer, + Clock, DoubleSide, Mesh, MeshLambertMaterial, Object3D, PerspectiveCamera, SkeletonHelper, + SkinnedMesh, Texture, Vector3, } from "three"; @@ -14,20 +19,34 @@ 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"; +import { + create_animation_clip, + PSO_FRAME_RATE, +} from "../../../core/rendering/conversion/ninja_animation"; +import { NjMotion } from "../../../core/data_formats/parsing/ninja/motion"; export class ModelRenderer extends Renderer implements Disposable { - private nj_object?: NjObject; - private object_3d?: Object3D; + private readonly perspective_camera: PerspectiveCamera; + private readonly disposables: Disposable[] = []; + private readonly clock = new Clock(); + private mesh?: Object3D; private skeleton_helper?: SkeletonHelper; - private perspective_camera: PerspectiveCamera; - private disposables: Disposable[] = []; + private animation?: { + mixer: AnimationMixer; + clip: AnimationClip; + action: AnimationAction; + }; 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)); + this.disposables.push( + model_store.current_nj_data.observe(this.nj_object_changed), + model_store.current_nj_motion.observe(this.nj_motion_changed), + model_store.show_skeleton.observe(this.show_skeleton_changed), + ); } set_size(width: number, height: number): void { @@ -42,102 +61,124 @@ export class ModelRenderer extends Renderer implements Disposable { } 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(); - // } + if (this.animation) { + this.animation.mixer.update(this.clock.getDelta()); + // this.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(); - // } + if (this.animation && !this.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(); + private nj_object_changed = (nj_data?: { nj_object: NjObject; has_skeleton: boolean }) => { + if (this.mesh) { + this.scene.remove(this.mesh); + this.mesh = undefined; + this.scene.remove(this.skeleton_helper!); + this.skeleton_helper = undefined; + } + + if (this.animation) { + this.animation.mixer.stopAllAction(); + if (this.mesh) this.animation.mixer.uncacheRoot(this.mesh); + this.animation = undefined; + } if (nj_data) { const { nj_object, has_skeleton } = nj_data; - if (nj_object !== this.nj_object) { - this.nj_object = nj_object; + let mesh: Mesh; - if (nj_object) { - let mesh: Mesh; + // TODO: + const textures: Texture[] | undefined = Math.random() > 1 ? [] : undefined; - const materials = - textures && - textures.map( - tex => - new MeshLambertMaterial({ - skinning: has_skeleton, - map: tex, - side: DoubleSide, - alphaTest: 0.5, - }), - ); + 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 (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); } - if (this.skeleton_helper) { - this.skeleton_helper.visible = model_store.show_skeleton.get(); - } + // 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); - // 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.mesh = mesh; + this.scene.add(mesh); + + this.skeleton_helper = new SkeletonHelper(mesh); + 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.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; + private nj_motion_changed = (nj_motion?: NjMotion) => { + let mixer!: AnimationMixer; + + if (this.animation) { + this.animation.mixer.stopAllAction(); + mixer = this.animation.mixer; } - 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)); + const nj_data = model_store.current_nj_data.get(); + + if (!this.mesh || !(this.mesh instanceof SkinnedMesh) || !nj_motion || !nj_data) return; + + if (!this.animation) { + mixer = new AnimationMixer(this.mesh); } - this.object_3d = object_3d; + const clip = create_animation_clip(nj_data.nj_object, nj_motion); + + this.animation = { + mixer, + clip, + action: mixer.clipAction(clip), + }; + + this.clock.start(); + this.animation.action.play(); + // TODO: + // this.animation_playing = true; + // this.animation_frame_count = Math.round(PSO_FRAME_RATE * clip.duration) + 1; + this.schedule_render(); + }; + + private show_skeleton_changed = (show_skeleton: boolean) => { + if (this.skeleton_helper) { + this.skeleton_helper.visible = show_skeleton; + this.schedule_render(); + } + }; + + private update(): void { + // 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(); + // } + this.schedule_render(); } } diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts index 544c5da3..cb9baf05 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/new/viewer/stores/ModelStore.ts @@ -1,12 +1,14 @@ -import { Clock } from "three"; import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; import { Endianness } from "../../../core/data_formats/Endianness"; import { NjMotion, parse_njm } from "../../../core/data_formats/parsing/ninja/motion"; import { NjObject, parse_nj, parse_xj } 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 { Property } from "../../core/observable/Property"; +import { + get_character_class_animation_data, + get_character_class_data, +} from "../../../viewer/loading/character_class"; import { Disposable } from "../../core/gui/Disposable"; import { read_file } from "../../../core/read_file"; import { create_animation_clip } from "../../../core/rendering/conversion/ninja_animation"; @@ -39,11 +41,9 @@ class ModelStore implements Disposable { .fill(undefined) .map((_, i) => new CharacterClassAnimation(i, `Animation ${i + 1}`)); - readonly clock = new Clock(); + readonly current_model = new Property(undefined); - readonly current_model = new Observable(undefined); - - readonly current_nj_data = new Observable< + readonly current_nj_data = new Property< | { nj_object: NjObject; bone_count: number; @@ -52,25 +52,24 @@ class ModelStore implements Disposable { | undefined >(undefined); - readonly current_animation = new Observable(undefined); + readonly current_animation = new Property(undefined); + + readonly current_nj_motion = new Property(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); + readonly show_skeleton = new Property(false); private disposables: Disposable[] = []; constructor() { - this.disposables.push(this.current_model.observe(this.load_model)); + this.disposables.push( + this.current_model.observe(this.load_model), + this.current_animation.observe(this.load_animation), + ); } dispose(): void { @@ -94,26 +93,16 @@ class ModelStore implements Disposable { // } // }; - // 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 { - this.current_model.set(undefined); - this.current_nj_data.set(undefined); - this.current_animation.set(undefined); - const buffer = await read_file(file); const cursor = new ArrayBufferCursor(buffer, Endianness.Little); if (file.name.endsWith(".nj")) { + this.current_model.set(undefined); + this.current_nj_data.set(undefined); + const nj_object = parse_nj(cursor)[0]; this.current_nj_data.set({ @@ -122,6 +111,9 @@ class ModelStore implements Disposable { has_skeleton: true, }); } else if (file.name.endsWith(".xj")) { + this.current_model.set(undefined); + this.current_nj_data.set(undefined); + const nj_object = parse_xj(cursor)[0]; this.current_nj_data.set({ @@ -129,11 +121,15 @@ class ModelStore implements Disposable { bone_count: 0, has_skeleton: 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(".njm")) { + this.current_animation.set(undefined); + this.current_nj_motion.set(undefined); + + const nj_data = this.current_nj_data.get(); + + if (nj_data) { + this.current_nj_motion.set(parse_njm(cursor, nj_data.bone_count)); + } // } else if (file.name.endsWith(".xvm")) { // if (this.current_model) { // const xvm = parse_xvm(cursor); @@ -175,39 +171,11 @@ class ModelStore implements Disposable { // } // }; - // 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) => { + this.current_animation.set(undefined); + 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; - // } + const nj_object = await this.get_nj_object(model); this.current_nj_data.set({ nj_object, @@ -220,27 +188,27 @@ class ModelStore implements Disposable { } }; - private async get_player_nj_object(model: CharacterClassModel): Promise { + private async get_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 = this.get_all_nj_objects(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"); + private async get_all_nj_objects(model: CharacterClassModel): Promise { + const body_data = await get_character_class_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_data = await get_character_class_data(model.name, "Head", 0); const head = parse_nj(new ArrayBufferCursor(head_data, Endianness.Little))[0]; if (head) { @@ -248,7 +216,7 @@ class ModelStore implements Disposable { } if (model.hair_styles_count > 0) { - const hair_data = await get_player_data(model.name, "Hair", 0); + const hair_data = await get_character_class_data(model.name, "Hair", 0); const hair = parse_nj(new ArrayBufferCursor(hair_data, Endianness.Little))[0]; if (hair) { @@ -256,7 +224,7 @@ class ModelStore implements Disposable { } if (model.hair_styles_with_accessory.has(0)) { - const accessory_data = await get_player_data(model.name, "Accessory", 0); + const accessory_data = await get_character_class_data(model.name, "Accessory", 0); const accessory = parse_nj( new ArrayBufferCursor(accessory_data, Endianness.Little), )[0]; @@ -280,24 +248,34 @@ class ModelStore implements Disposable { } } - // 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 load_animation = async (animation?: CharacterClassAnimation) => { + const nj_data = this.current_nj_data.get(); + + if (nj_data && animation) { + this.current_nj_motion.set(await this.get_nj_motion(animation, nj_data.bone_count)); + } else { + this.current_nj_motion.set(undefined); + } + }; + + private async get_nj_motion( + animation: CharacterClassAnimation, + bone_count: number, + ): Promise { + let nj_motion = nj_motion_cache.get(animation.id); + + if (nj_motion) { + return nj_motion; + } else { + nj_motion = get_character_class_animation_data(animation.id).then(motion_data => + parse_njm(new ArrayBufferCursor(motion_data, Endianness.Little), bone_count), + ); + + nj_motion_cache.set(animation.id, nj_motion); + return nj_motion; + } + } + // private set_textures = (textures: Texture[]) => { // this.set_obj3d(textures); // }; diff --git a/src/viewer/loading/player.ts b/src/viewer/loading/character_class.ts similarity index 52% rename from src/viewer/loading/player.ts rename to src/viewer/loading/character_class.ts index b5e5969d..a0d10f74 100644 --- a/src/viewer/loading/player.ts +++ b/src/viewer/loading/character_class.ts @@ -1,19 +1,19 @@ import { load_array_buffer } from "../../core/loading"; -export async function get_player_data( +export async function get_character_class_data( player_class: string, body_part: string, no?: number, ): Promise { - return await load_array_buffer(player_class_to_url(player_class, body_part, no)); + return await load_array_buffer(character_class_to_url(player_class, body_part, no)); } -export async function get_player_animation_data(animation_id: number): Promise { +export async function get_character_class_animation_data(animation_id: number): Promise { return await load_array_buffer( `/player/animation/animation_${animation_id.toString().padStart(3, "0")}.njm`, ); } -function player_class_to_url(player_class: string, body_part: string, no?: number): string { +function character_class_to_url(player_class: string, body_part: string, no?: number): string { return `/player/${player_class}${body_part}${no == undefined ? "" : no}.nj`; } diff --git a/src/viewer/stores/ModelViewerStore.ts b/src/viewer/stores/ModelViewerStore.ts index 7032d249..1ef2bf10 100644 --- a/src/viewer/stores/ModelViewerStore.ts +++ b/src/viewer/stores/ModelViewerStore.ts @@ -16,7 +16,7 @@ import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCur import { NjModel, NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja"; import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motion"; import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { get_player_animation_data, get_player_data } from "../loading/player"; +import { get_character_class_animation_data, get_character_class_data } from "../loading/character_class"; import { read_file } from "../../core/read_file"; import { create_skinned_mesh, create_mesh } from "../../core/rendering/conversion/create_mesh"; import { @@ -232,14 +232,14 @@ class ModelViewerStore { } private async get_all_assets(model: PlayerModel): Promise> { - const body_data = await get_player_data(model.name, "Body"); + const body_data = await get_character_class_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_data = await get_character_class_data(model.name, "Head", 0); const head = parse_nj(new ArrayBufferCursor(head_data, Endianness.Little))[0]; if (head) { @@ -247,7 +247,7 @@ class ModelViewerStore { } if (model.hair_styles_count > 0) { - const hair_data = await get_player_data(model.name, "Hair", 0); + const hair_data = await get_character_class_data(model.name, "Hair", 0); const hair = parse_nj(new ArrayBufferCursor(hair_data, Endianness.Little))[0]; if (hair) { @@ -255,7 +255,7 @@ class ModelViewerStore { } if (model.hair_styles_with_accessory.has(0)) { - const accessory_data = await get_player_data(model.name, "Accessory", 0); + const accessory_data = await get_character_class_data(model.name, "Accessory", 0); const accessory = parse_nj( new ArrayBufferCursor(accessory_data, Endianness.Little), )[0]; @@ -275,7 +275,7 @@ class ModelViewerStore { if (nj_motion) { return nj_motion; } else { - nj_motion = get_player_animation_data(animation.id).then(motion_data => + nj_motion = get_character_class_animation_data(animation.id).then(motion_data => parse_njm( new ArrayBufferCursor(motion_data, Endianness.Little), this.current_bone_count, diff --git a/webpack.dev.js b/webpack.dev.js index 626d1515..010e9877 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -1,5 +1,4 @@ const common = require("./webpack.common.js"); -const antd_theme = require("./antd_theme.js"); const path = require("path"); const merge = require("webpack-merge"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); @@ -38,9 +37,6 @@ module.exports = merge(common, { loader: "css-loader", options: { sourceMap: true, - // modules: { - // localIdentName: "[path][name]__[local]", - // }, }, }, ], @@ -58,27 +54,6 @@ module.exports = merge(common, { }, ], }, - { - test: /\.less$/, - include: /antd/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: "css-loader", - options: { - sourceMap: true, - }, - }, - { - loader: "less-loader", - options: { - sourceMap: true, - javascriptEnabled: true, - modifyVars: antd_theme, - }, - }, - ], - }, { test: /\.(png|svg|jpg|gif)$/, use: ["file-loader"], diff --git a/webpack.prod.js b/webpack.prod.js index be048700..b68f9de6 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -1,5 +1,4 @@ const common = require("./webpack.common.js"); -const antd_theme = require("./antd_theme.js"); const path = require("path"); const merge = require("webpack-merge"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); @@ -19,7 +18,7 @@ module.exports = merge(common, { cacheGroups: { styles: { name: "style", - test: /\.(css|less)$/, + test: /\.css$/, chunks: "all", enforce: true, }, @@ -40,39 +39,8 @@ module.exports = merge(common, { }, { test: /\.css$/, - exclude: /node_modules/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: "css-loader", - options: { - modules: { - localIdentName: "[local]--[hash:base64:5]", - }, - }, - }, - ], - }, - { - test: /\.css$/, - include: /node_modules/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, - { - test: /\.less$/, - include: /antd/, - use: [ - MiniCssExtractPlugin.loader, - "css-loader", - { - loader: "less-loader", - options: { - javascriptEnabled: true, - modifyVars: antd_theme, - }, - }, - ], - }, { test: /\.(png|svg|jpg|gif)$/, use: ["file-loader"], From 429595b513d32a8443a57e618c99b7f58d24bed1 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 15:19:44 +0200 Subject: [PATCH 04/50] Model viewer now completely build on the new UI system. --- src/new/core/gui/Button.ts | 10 +- src/new/core/gui/CheckBox.css | 6 - src/new/core/gui/CheckBox.ts | 32 ++-- src/new/core/gui/Control.ts | 7 + .../gui/{FileInput.css => FileButton.css} | 2 +- .../core/gui/{FileInput.ts => FileButton.ts} | 18 ++- src/new/core/gui/Input.css | 23 +++ src/new/core/gui/Label.css | 3 + src/new/core/gui/Label.ts | 37 +++++ src/new/core/gui/LabelledControl.ts | 35 +++++ src/new/core/gui/LazyView.ts | 2 +- src/new/core/gui/NumberInput.css | 3 + src/new/core/gui/NumberInput.ts | 50 +++++++ src/new/core/gui/TabContainer.ts | 2 +- src/new/core/gui/ToolBar.css | 13 +- src/new/core/gui/ToolBar.ts | 19 ++- src/new/core/gui/View.ts | 21 ++- src/new/core/gui/dom.ts | 2 +- .../core/{gui => observable}/Disposable.ts | 0 src/new/core/observable/Emitter.ts | 5 + src/new/core/observable/MappedProperty.ts | 57 +++++++ src/new/core/observable/Observable.ts | 32 +--- src/new/core/observable/Property.ts | 27 ++-- src/new/core/observable/SimpleEmitter.ts | 34 +++++ src/new/core/observable/SimpleProperty.ts | 54 +++++++ src/new/core/observable/WritableProperty.ts | 19 +++ src/new/core/observable/index.ts | 12 ++ src/new/core/stores/GuiStore.ts | 7 +- src/new/index.css | 7 + src/new/index.ts | 2 +- src/new/viewer/gui/ModelView.ts | 64 ++++++-- src/new/viewer/rendering/ModelRenderer.ts | 64 ++++++-- src/new/viewer/stores/ModelStore.ts | 139 +++++++----------- 33 files changed, 601 insertions(+), 207 deletions(-) delete mode 100644 src/new/core/gui/CheckBox.css create mode 100644 src/new/core/gui/Control.ts rename src/new/core/gui/{FileInput.css => FileButton.css} (85%) rename src/new/core/gui/{FileInput.ts => FileButton.ts} (58%) create mode 100644 src/new/core/gui/Input.css create mode 100644 src/new/core/gui/Label.css create mode 100644 src/new/core/gui/Label.ts create mode 100644 src/new/core/gui/LabelledControl.ts create mode 100644 src/new/core/gui/NumberInput.css create mode 100644 src/new/core/gui/NumberInput.ts rename src/new/core/{gui => observable}/Disposable.ts (100%) create mode 100644 src/new/core/observable/Emitter.ts create mode 100644 src/new/core/observable/MappedProperty.ts create mode 100644 src/new/core/observable/SimpleEmitter.ts create mode 100644 src/new/core/observable/SimpleProperty.ts create mode 100644 src/new/core/observable/WritableProperty.ts create mode 100644 src/new/core/observable/index.ts diff --git a/src/new/core/gui/Button.ts b/src/new/core/gui/Button.ts index 525d8b71..2becce54 100644 --- a/src/new/core/gui/Button.ts +++ b/src/new/core/gui/Button.ts @@ -2,17 +2,19 @@ import { create_el } from "./dom"; import { View } from "./View"; import "./Button.css"; import { Observable } from "../observable/Observable"; +import { emitter } from "../observable"; export class Button extends View { - element: HTMLButtonElement = create_el("button", "core_Button"); + readonly element: HTMLButtonElement = create_el("button", "core_Button"); + + private readonly _click = emitter(); + readonly click: Observable = this._click; constructor(text: string) { super(); this.element.textContent = text; - this.element.onclick = (e: MouseEvent) => this.click.fire(e, undefined); + this.element.onclick = (e: MouseEvent) => this._click.emit(e, undefined); } - - click = new Observable(); } diff --git a/src/new/core/gui/CheckBox.css b/src/new/core/gui/CheckBox.css deleted file mode 100644 index 53379309..00000000 --- a/src/new/core/gui/CheckBox.css +++ /dev/null @@ -1,6 +0,0 @@ -.core_CheckBox { - display: inline-flex; - flex-direction: row; - height: 26px; - align-items: center; -} diff --git a/src/new/core/gui/CheckBox.ts b/src/new/core/gui/CheckBox.ts index 5f485234..3e64461f 100644 --- a/src/new/core/gui/CheckBox.ts +++ b/src/new/core/gui/CheckBox.ts @@ -1,21 +1,27 @@ -import { View } from "./View"; import { create_el } from "./dom"; -import "./CheckBox.css"; -import { Property } from "../observable/Property"; +import { WritableProperty } from "../observable/WritableProperty"; +import { property } from "../observable"; +import { LabelledControl } from "./LabelledControl"; -export class CheckBox extends View { - private input: HTMLInputElement = create_el("input"); +export class CheckBox extends LabelledControl { + readonly element: HTMLInputElement = create_el("input", "core_CheckBox"); - element: HTMLLabelElement = create_el("label", "core_CheckBox"); + readonly checked: WritableProperty = property(false); - constructor(text: string) { - super(); + readonly preferred_label_position = "right"; - this.input.type = "checkbox"; - this.input.onchange = () => this.checked.set(this.input.checked); + constructor(checked: boolean = false, label?: string) { + super(label); - this.element.append(this.input, text); + this.element.type = "checkbox"; + this.element.onchange = () => this.checked.set(this.element.checked); + + this.disposables( + this.checked.observe(checked => (this.element.checked = checked)), + + this.enabled.observe(enabled => (this.element.disabled = !enabled)), + ); + + this.checked.set(checked); } - - checked = new Property(false); } diff --git a/src/new/core/gui/Control.ts b/src/new/core/gui/Control.ts new file mode 100644 index 00000000..82cda0df --- /dev/null +++ b/src/new/core/gui/Control.ts @@ -0,0 +1,7 @@ +import { View } from "./View"; +import { WritableProperty } from "../observable/WritableProperty"; +import { property } from "../observable"; + +export abstract class Control extends View { + readonly enabled: WritableProperty = property(true); +} diff --git a/src/new/core/gui/FileInput.css b/src/new/core/gui/FileButton.css similarity index 85% rename from src/new/core/gui/FileInput.css rename to src/new/core/gui/FileButton.css index 5418a1db..85601698 100644 --- a/src/new/core/gui/FileInput.css +++ b/src/new/core/gui/FileButton.css @@ -1,4 +1,4 @@ -.core_FileInput_input { +.core_FileButton_input { overflow: hidden; clip: rect(0, 0, 0, 0); position: absolute; diff --git a/src/new/core/gui/FileInput.ts b/src/new/core/gui/FileButton.ts similarity index 58% rename from src/new/core/gui/FileInput.ts rename to src/new/core/gui/FileButton.ts index 6b2e82d1..3749b4e3 100644 --- a/src/new/core/gui/FileInput.ts +++ b/src/new/core/gui/FileButton.ts @@ -1,13 +1,17 @@ import { create_el } from "./dom"; import { View } from "./View"; -import "./FileInput.css"; +import "./FileButton.css"; import "./Button.css"; +import { property } from "../observable"; import { Property } from "../observable/Property"; -export class FileInput extends View { - private input: HTMLInputElement = create_el("input", "core_FileInput_input"); +export class FileButton extends View { + readonly element: HTMLLabelElement = create_el("label", "core_FileButton core_Button"); - element: HTMLLabelElement = create_el("label", "core_FileInput core_Button"); + private readonly _files = property([]); + readonly files: Property = this._files; + + private input: HTMLInputElement = create_el("input", "core_FileButton_input"); constructor(text: string, accept: string = "") { super(); @@ -16,15 +20,13 @@ export class FileInput extends View { this.input.accept = accept; this.input.onchange = () => { if (this.input.files && this.input.files.length) { - this.files.set([...this.input.files!]); + this._files.set([...this.input.files!]); } else { - this.files.set([]); + this._files.set([]); } }; this.element.textContent = text; this.element.append(this.input); } - - readonly files = new Property([]); } diff --git a/src/new/core/gui/Input.css b/src/new/core/gui/Input.css new file mode 100644 index 00000000..dbb85b0d --- /dev/null +++ b/src/new/core/gui/Input.css @@ -0,0 +1,23 @@ +.core_Input { + box-sizing: border-box; + height: 26px; + padding: 0 3px; + border: solid 1px var(--border-color); + background-color: var(--input-bg-color); + color: var(--text-color); + outline: none; +} + +.core_Input:hover { + border: solid 1px var(--border-color-hover); +} + +.core_Input:focus { + border: solid 1px var(--border-color-focus); +} + +.core_Input:disabled { + color: var(--text-color-disabled); + background-color: var(--input-bg-color-disabled); + border: solid 1px var(--border-color); +} diff --git a/src/new/core/gui/Label.css b/src/new/core/gui/Label.css new file mode 100644 index 00000000..f998b694 --- /dev/null +++ b/src/new/core/gui/Label.css @@ -0,0 +1,3 @@ +.core_Label.disabled { + color: var(--text-color-disabled); +} diff --git a/src/new/core/gui/Label.ts b/src/new/core/gui/Label.ts new file mode 100644 index 00000000..919ef8cb --- /dev/null +++ b/src/new/core/gui/Label.ts @@ -0,0 +1,37 @@ +import { View } from "./View"; +import { create_el } from "./dom"; +import { WritableProperty } from "../observable/WritableProperty"; +import "./Label.css"; +import { property } from "../observable"; +import { Property } from "../observable/Property"; + +export class Label extends View { + readonly element = create_el("label", "core_Label"); + + set for(id: string) { + this.element.htmlFor = id; + } + + readonly enabled: WritableProperty = property(true); + + constructor(text: string | Property) { + super(); + + if (typeof text === "string") { + this.element.append(text); + } else { + this.element.append(text.get()); + this.disposable(text.observe(text => (this.element.textContent = text))); + } + + this.disposables( + this.enabled.observe(enabled => { + if (enabled) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } + }), + ); + } +} diff --git a/src/new/core/gui/LabelledControl.ts b/src/new/core/gui/LabelledControl.ts new file mode 100644 index 00000000..9bd8b857 --- /dev/null +++ b/src/new/core/gui/LabelledControl.ts @@ -0,0 +1,35 @@ +import { Label } from "./Label"; +import { Control } from "./Control"; + +export abstract class LabelledControl extends Control { + abstract readonly preferred_label_position: "left" | "right"; + + private readonly _label_text: string; + private _label?: Label; + + get label(): Label { + if (!this._label) { + this._label = this.disposable(new Label(this._label_text)); + + if (!this.id) { + this._label.for = this.id = unique_id(); + } + + this._label.enabled.bind_bi(this.enabled); + } + + return this._label; + } + + protected constructor(label: string | undefined) { + super(); + + this._label_text = label || ""; + } +} + +let id = 0; + +function unique_id(): string { + return String(id++); +} diff --git a/src/new/core/gui/LazyView.ts b/src/new/core/gui/LazyView.ts index c1a89b77..e05d43cf 100644 --- a/src/new/core/gui/LazyView.ts +++ b/src/new/core/gui/LazyView.ts @@ -4,7 +4,7 @@ import { Resizable } from "./Resizable"; import { ResizableView } from "./ResizableView"; export class LazyView extends ResizableView { - element = create_el("div", "core_LazyView"); + readonly element = create_el("div", "core_LazyView"); private _visible = false; diff --git a/src/new/core/gui/NumberInput.css b/src/new/core/gui/NumberInput.css new file mode 100644 index 00000000..3f9981f0 --- /dev/null +++ b/src/new/core/gui/NumberInput.css @@ -0,0 +1,3 @@ +.core_NumberInput { + text-align: right; +} \ No newline at end of file diff --git a/src/new/core/gui/NumberInput.ts b/src/new/core/gui/NumberInput.ts new file mode 100644 index 00000000..deb9c2a0 --- /dev/null +++ b/src/new/core/gui/NumberInput.ts @@ -0,0 +1,50 @@ +import "./NumberInput.css"; +import "./Input.css"; +import { create_el } from "./dom"; +import { WritableProperty } from "../observable/WritableProperty"; +import { property } from "../observable"; +import { LabelledControl } from "./LabelledControl"; +import { is_property, Property } from "../observable/Property"; + +export class NumberInput extends LabelledControl { + readonly element: HTMLInputElement = create_el("input", "core_NumberInput core_Input"); + + readonly value: WritableProperty = property(0); + + readonly preferred_label_position = "left"; + + constructor( + value = 0, + label?: string, + min: number | Property = -Infinity, + max: number | Property = Infinity, + step: number | Property = 1, + ) { + super(label); + + this.element.type = "number"; + this.element.valueAsNumber = value; + this.element.style.width = "50px"; + + this.set_prop("min", min); + this.set_prop("max", max); + this.set_prop("step", step); + + this.element.onchange = () => this.value.set(this.element.valueAsNumber); + + this.disposables( + this.value.observe(value => (this.element.valueAsNumber = value)), + + this.enabled.observe(enabled => (this.element.disabled = !enabled)), + ); + } + + private set_prop(prop: "min" | "max" | "step", value: T | Property): void { + if (is_property(value)) { + this.element[prop] = String(value.get()); + this.disposable(value.observe(v => (this.element[prop] = String(v)))); + } else { + this.element[prop] = String(value); + } + } +} diff --git a/src/new/core/gui/TabContainer.ts b/src/new/core/gui/TabContainer.ts index 1ea0e02f..3d017d2c 100644 --- a/src/new/core/gui/TabContainer.ts +++ b/src/new/core/gui/TabContainer.ts @@ -16,7 +16,7 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; const BAR_HEIGHT = 28; export class TabContainer extends ResizableView { - element = create_el("div", "core_TabContainer"); + readonly element = create_el("div", "core_TabContainer"); private tabs: TabInfo[] = []; private bar_element = create_el("div", "core_TabContainer_Bar"); diff --git a/src/new/core/gui/ToolBar.css b/src/new/core/gui/ToolBar.css index 349bd89a..439c1b1d 100644 --- a/src/new/core/gui/ToolBar.css +++ b/src/new/core/gui/ToolBar.css @@ -2,12 +2,23 @@ box-sizing: border-box; display: flex; flex-direction: row; + align-items: center; padding-top: 1px; border-bottom: solid var(--border-color) 1px; } .core_ToolBar > * { - margin: 2px; + margin: 2px 4px; +} + +.core_ToolBar > .core_ToolBar_group { + display: flex; + flex-direction: row; + align-items: center; +} + +.core_ToolBar > .core_ToolBar_group > * { + margin: 0 2px; } .core_ToolBar .core_Button { diff --git a/src/new/core/gui/ToolBar.ts b/src/new/core/gui/ToolBar.ts index 34fa3bdb..7088114c 100644 --- a/src/new/core/gui/ToolBar.ts +++ b/src/new/core/gui/ToolBar.ts @@ -1,10 +1,11 @@ import { View } from "./View"; import { create_el } from "./dom"; import "./ToolBar.css"; +import { LabelledControl } from "./LabelledControl"; export class ToolBar extends View { readonly element = create_el("div", "core_ToolBar"); - readonly height = 32; + readonly height = 34; constructor(...children: View[]) { super(); @@ -12,8 +13,20 @@ export class ToolBar extends View { this.element.style.height = `${this.height}px`; for (const child of children) { - this.element.append(child.element); - this.disposable(child); + if (child instanceof LabelledControl) { + const group = create_el("div", "core_ToolBar_group"); + + if (child.preferred_label_position === "left") { + group.append(child.label.element, child.element); + } else { + group.append(child.element, child.label.element); + } + + this.element.append(group); + } else { + this.element.append(child.element); + this.disposable(child); + } } } } diff --git a/src/new/core/gui/View.ts b/src/new/core/gui/View.ts index af952a85..8d75d8ea 100644 --- a/src/new/core/gui/View.ts +++ b/src/new/core/gui/View.ts @@ -1,18 +1,29 @@ -import { Disposable } from "./Disposable"; +import { Disposable } from "../observable/Disposable"; export abstract class View implements Disposable { abstract readonly element: HTMLElement; - private disposables: Disposable[] = []; + get id(): string { + return this.element.id; + } + + set id(id: string) { + this.element.id = id; + } + + private disposable_list: Disposable[] = []; protected disposable(disposable: T): T { - this.disposables.push(disposable); + this.disposable_list.push(disposable); return disposable; } + protected disposables(...disposables: Disposable[]): void { + this.disposable_list.push(...disposables); + } + dispose(): void { this.element.remove(); - this.disposables.forEach(d => d.dispose()); - this.disposables.splice(0, this.disposables.length); + this.disposable_list.splice(0, this.disposable_list.length).forEach(d => d.dispose()); } } diff --git a/src/new/core/gui/dom.ts b/src/new/core/gui/dom.ts index ba0ad91c..ea4b0d46 100644 --- a/src/new/core/gui/dom.ts +++ b/src/new/core/gui/dom.ts @@ -1,4 +1,4 @@ -import { Disposable } from "./Disposable"; +import { Disposable } from "../observable/Disposable"; export function create_el( tag_name: string, diff --git a/src/new/core/gui/Disposable.ts b/src/new/core/observable/Disposable.ts similarity index 100% rename from src/new/core/gui/Disposable.ts rename to src/new/core/observable/Disposable.ts diff --git a/src/new/core/observable/Emitter.ts b/src/new/core/observable/Emitter.ts new file mode 100644 index 00000000..54a3a53b --- /dev/null +++ b/src/new/core/observable/Emitter.ts @@ -0,0 +1,5 @@ +import { Observable } from "./Observable"; + +export interface Emitter extends Observable { + emit(event: E, meta: M): void; +} diff --git a/src/new/core/observable/MappedProperty.ts b/src/new/core/observable/MappedProperty.ts new file mode 100644 index 00000000..238b4dde --- /dev/null +++ b/src/new/core/observable/MappedProperty.ts @@ -0,0 +1,57 @@ +import { SimpleEmitter } from "./SimpleEmitter"; +import { Disposable } from "./Disposable"; +import { Property, PropertyMeta } from "./Property"; + +/** + * Starts observing its origin when the first observer on this property is registered. + * Stops observing its origin when the last observer on this property is disposed. + * This way no extra disposables need to be managed when {@link Property.map} is used. + */ +export class MappedProperty extends SimpleEmitter> implements Property { + readonly is_property = true; + + private origin_disposable?: Disposable; + private value?: T; + + constructor(private origin: Property, private f: (value: S) => T) { + super(); + } + + observe(observer: (event: T, meta: PropertyMeta) => void): Disposable { + const disposable = super.observe(observer); + + if (this.origin_disposable == undefined) { + this.value = this.f(this.origin.get()); + + this.origin_disposable = this.origin.observe(origin_value => { + const old_value = this.value as T; + this.value = this.f(origin_value); + + this.emit(this.value, { old_value }); + }); + } + + return { + dispose: () => { + disposable.dispose(); + + if (this.observers.length === 0) { + this.origin_disposable!.dispose(); + this.origin_disposable = undefined; + } + }, + }; + } + + get(): T { + if (this.origin_disposable) { + return this.value as T; + } else { + return this.f(this.origin.get()); + } + } + + map(f: (element: T) => U): Property { + return new MappedProperty(this, f); + } +} diff --git a/src/new/core/observable/Observable.ts b/src/new/core/observable/Observable.ts index 1dc400e1..dffd4e3e 100644 --- a/src/new/core/observable/Observable.ts +++ b/src/new/core/observable/Observable.ts @@ -1,31 +1,5 @@ -import { Disposable } from "../gui/Disposable"; +import { Disposable } from "./Disposable"; -export class Observable { - private readonly observers: ((event: E, meta: M) => void)[] = []; - - fire(event: E, meta: M): void { - for (const observer of this.observers) { - try { - observer(event, meta); - } catch (e) { - console.error(e); - } - } - } - - observe(observer: (event: E, meta: M) => 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); - } - }, - }; - } +export interface Observable { + observe(observer: (event: E, meta: M) => void): Disposable; } diff --git a/src/new/core/observable/Property.ts b/src/new/core/observable/Property.ts index d613801a..093be6dc 100644 --- a/src/new/core/observable/Property.ts +++ b/src/new/core/observable/Property.ts @@ -1,22 +1,15 @@ import { Observable } from "./Observable"; -export class Property extends Observable { - private value: T; +export interface Property extends Observable> { + readonly is_property: true; - constructor(value: T) { - super(); - this.value = value; - } + get(): T; - get(): T { - return this.value; - } - - set(value: T): void { - if (value !== this.value) { - const old_value = this.value; - this.value = value; - this.fire(value, { old_value }); - } - } + map(f: (element: T) => U): Property; +} + +export type PropertyMeta = { old_value: T }; + +export function is_property(observable: any): observable is Property { + return (observable as any).is_property; } diff --git a/src/new/core/observable/SimpleEmitter.ts b/src/new/core/observable/SimpleEmitter.ts new file mode 100644 index 00000000..3c303dad --- /dev/null +++ b/src/new/core/observable/SimpleEmitter.ts @@ -0,0 +1,34 @@ +import { Disposable } from "./Disposable"; +import Logger from "js-logger"; + +const logger = Logger.get("core/observable/SimpleEmitter"); + +export class SimpleEmitter { + protected readonly observers: ((event: E, meta: M) => void)[] = []; + + emit(event: E, meta: M): void { + for (const observer of this.observers) { + try { + observer(event, meta); + } catch (e) { + logger.error("Observer threw error.", e); + } + } + } + + observe(observer: (event: E, meta: M) => 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/observable/SimpleProperty.ts b/src/new/core/observable/SimpleProperty.ts new file mode 100644 index 00000000..ce8ddbd9 --- /dev/null +++ b/src/new/core/observable/SimpleProperty.ts @@ -0,0 +1,54 @@ +import { SimpleEmitter } from "./SimpleEmitter"; +import { Disposable } from "./Disposable"; +import { Observable } from "./Observable"; +import { WritableProperty } from "./WritableProperty"; +import { Property, PropertyMeta, is_property } from "./Property"; +import { MappedProperty } from "./MappedProperty"; + +export class SimpleProperty extends SimpleEmitter> + implements WritableProperty { + readonly is_property = true; + readonly is_writable_property = true; + + private value: T; + + constructor(value: T) { + super(); + this.value = value; + } + + get(): T { + return this.value; + } + + set(value: T): void { + if (value !== this.value) { + const old_value = this.value; + this.value = value; + this.emit(value, { old_value }); + } + } + + bind(observable: Observable): Disposable { + if (is_property(observable)) { + this.set(observable.get()); + } + + return observable.observe(v => this.set(v)); + } + + bind_bi(property: WritableProperty): Disposable { + const bind_1 = this.bind(property); + const bind_2 = property.bind(this); + return { + dispose(): void { + bind_1.dispose(); + bind_2.dispose(); + }, + }; + } + + map(f: (element: T) => U): Property { + return new MappedProperty(this, f); + } +} diff --git a/src/new/core/observable/WritableProperty.ts b/src/new/core/observable/WritableProperty.ts new file mode 100644 index 00000000..64f464b2 --- /dev/null +++ b/src/new/core/observable/WritableProperty.ts @@ -0,0 +1,19 @@ +import { Property } from "./Property"; +import { Observable } from "./Observable"; +import { Disposable } from "./Disposable"; + +export interface WritableProperty extends Property { + is_writable_property: true; + + set(value: T): void; + + bind(observable: Observable): Disposable; + + bind_bi(property: WritableProperty): Disposable; +} + +export function is_writable_property( + observable: Observable, +): observable is WritableProperty { + return (observable as any).is_writable_property; +} diff --git a/src/new/core/observable/index.ts b/src/new/core/observable/index.ts new file mode 100644 index 00000000..864fa6ca --- /dev/null +++ b/src/new/core/observable/index.ts @@ -0,0 +1,12 @@ +import { SimpleEmitter } from "./SimpleEmitter"; +import { WritableProperty } from "./WritableProperty"; +import { SimpleProperty } from "./SimpleProperty"; +import { Emitter } from "./Emitter"; + +export function emitter(): Emitter { + return new SimpleEmitter(); +} + +export function property(value: T): WritableProperty { + return new SimpleProperty(value); +} diff --git a/src/new/core/stores/GuiStore.ts b/src/new/core/stores/GuiStore.ts index 2e46be63..02aa892a 100644 --- a/src/new/core/stores/GuiStore.ts +++ b/src/new/core/stores/GuiStore.ts @@ -1,5 +1,6 @@ -import { Property } from "../observable/Property"; -import { Disposable } from "../gui/Disposable"; +import { WritableProperty } from "../observable/WritableProperty"; +import { Disposable } from "../observable/Disposable"; +import { property } from "../observable"; export enum GuiTool { Viewer, @@ -15,7 +16,7 @@ const GUI_TOOL_TO_STRING = new Map([ const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k])); class GuiStore implements Disposable { - tool = new Property(GuiTool.Viewer); + readonly tool: WritableProperty = property(GuiTool.Viewer); private hash_disposer = this.tool.observe(tool => { window.location.hash = `#/${gui_tool_to_string(tool)}`; diff --git a/src/new/index.css b/src/new/index.css index 2174d9e1..88450b65 100644 --- a/src/new/index.css +++ b/src/new/index.css @@ -1,9 +1,16 @@ :root { --bg-color: hsl(0, 0%, 20%); --text-color: hsl(0, 0%, 85%); + --text-color-disabled: hsl(0, 0%, 55%); --border-color: hsl(0, 0%, 30%); + --border-color-hover: hsl(0, 0%, 40%); + --border-color-focus: hsl(0, 0%, 50%); + --scrollbar-color: hsl(0, 0%, 17%); --scrollbar-thumb-color: hsl(0, 0%, 23%); + + --input-bg-color: hsl(0, 0%, 10%); + --input-bg-color-disabled: hsl(0, 0%, 15%); } * { diff --git a/src/new/index.ts b/src/new/index.ts index 269877fe..31788a71 100644 --- a/src/new/index.ts +++ b/src/new/index.ts @@ -1,5 +1,5 @@ import { ApplicationView } from "./application/gui/ApplicationView"; -import { Disposable } from "./core/gui/Disposable"; +import { Disposable } from "./core/observable/Disposable"; import "./index.css"; import { throttle } from "lodash"; diff --git a/src/new/viewer/gui/ModelView.ts b/src/new/viewer/gui/ModelView.ts index e0f658b4..cfc5a054 100644 --- a/src/new/viewer/gui/ModelView.ts +++ b/src/new/viewer/gui/ModelView.ts @@ -3,12 +3,15 @@ import { ResizableView } from "../../core/gui/ResizableView"; import { ToolBar } from "../../core/gui/ToolBar"; import "./ModelView.css"; import { model_store } from "../stores/ModelStore"; -import { Property } from "../../core/observable/Property"; +import { WritableProperty } from "../../core/observable/WritableProperty"; import { RendererView } from "../../core/gui/RendererView"; import { ModelRenderer } from "../rendering/ModelRenderer"; import { View } from "../../core/gui/View"; -import { FileInput } from "../../core/gui/FileInput"; +import { FileButton } from "../../core/gui/FileButton"; import { CheckBox } from "../../core/gui/CheckBox"; +import { NumberInput } from "../../core/gui/NumberInput"; +import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; +import { Label } from "../../core/gui/Label"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 150; @@ -59,11 +62,36 @@ export class ModelView extends ResizableView { } class ToolBarView extends View { - private readonly open_file_button = new FileInput("Open file...", ".nj, .njm, .xj"); - private readonly skeleton_checkbox = new CheckBox("Show skeleton"); + private readonly open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm"); + private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton"); + private readonly play_animation_checkbox = new CheckBox(true, "Play animation"); + private readonly animation_frame_rate_input = new NumberInput( + PSO_FRAME_RATE, + "Frame rate:", + 1, + 240, + 1, + ); + private readonly animation_frame_input = new NumberInput( + 1, + "Frame:", + 1, + model_store.animation_frame_count, + 1, + ); + private readonly animation_frame_count_label = new Label( + model_store.animation_frame_count.map(count => `/ ${count}`), + ); private readonly tool_bar = this.disposable( - new ToolBar(this.open_file_button, this.skeleton_checkbox), + new ToolBar( + this.open_file_button, + this.skeleton_checkbox, + this.play_animation_checkbox, + this.animation_frame_rate_input, + this.animation_frame_input, + this.animation_frame_count_label, + ), ); readonly element = this.tool_bar.element; @@ -75,16 +103,32 @@ class ToolBarView extends View { constructor() { super(); - this.disposable( + // Always-enabled controls. + this.disposables( this.open_file_button.files.observe(files => { if (files.length) model_store.load_file(files[0]); }), + + model_store.show_skeleton.bind(this.skeleton_checkbox.checked), ); - this.disposable( - this.skeleton_checkbox.checked.observe(checked => - model_store.show_skeleton.set(checked), + // Controls that are only enabled when an animation is selected. + const enabled = model_store.current_nj_motion.map(njm => njm != undefined); + + this.disposables( + this.play_animation_checkbox.enabled.bind(enabled), + model_store.animation_playing.bind_bi(this.play_animation_checkbox.checked), + + this.animation_frame_rate_input.enabled.bind(enabled), + model_store.animation_frame_rate.bind(this.animation_frame_rate_input.value), + + this.animation_frame_input.enabled.bind(enabled), + model_store.animation_frame.bind(this.animation_frame_input.value), + this.animation_frame_input.value.bind( + model_store.animation_frame.map(v => Math.round(v)), ), + + this.animation_frame_count_label.enabled.bind(enabled), ); } } @@ -105,7 +149,7 @@ class ModelSelectListView extends ResizableView { private selected_model?: T; private selected_element?: HTMLLIElement; - constructor(private models: T[], private selected: Property) { + constructor(private models: T[], private selected: WritableProperty) { super(); this.element.onclick = this.list_click; diff --git a/src/new/viewer/rendering/ModelRenderer.ts b/src/new/viewer/rendering/ModelRenderer.ts index 21adcb5b..10c428ad 100644 --- a/src/new/viewer/rendering/ModelRenderer.ts +++ b/src/new/viewer/rendering/ModelRenderer.ts @@ -15,7 +15,7 @@ import { } from "three"; import { Renderer } from "../../../core/rendering/Renderer"; import { model_store } from "../stores/ModelStore"; -import { Disposable } from "../../core/gui/Disposable"; +import { Disposable } from "../../core/observable/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"; @@ -24,6 +24,7 @@ import { PSO_FRAME_RATE, } from "../../../core/rendering/conversion/ninja_animation"; import { NjMotion } from "../../../core/data_formats/parsing/ninja/motion"; +import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; export class ModelRenderer extends Renderer implements Disposable { private readonly perspective_camera: PerspectiveCamera; @@ -43,9 +44,13 @@ export class ModelRenderer extends Renderer implements Disposable { this.perspective_camera = this.camera as PerspectiveCamera; this.disposables.push( - model_store.current_nj_data.observe(this.nj_object_changed), + model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), + model_store.current_xvm.observe(this.nj_data_or_xvm_changed), model_store.current_nj_motion.observe(this.nj_motion_changed), model_store.show_skeleton.observe(this.show_skeleton_changed), + model_store.animation_playing.observe(this.animation_playing_changed), + model_store.animation_frame_rate.observe(this.animation_frame_rate_changed), + model_store.animation_frame.observe(this.animation_frame_changed), ); } @@ -63,18 +68,18 @@ export class ModelRenderer extends Renderer implements Disposable { protected render(): void { if (this.animation) { this.animation.mixer.update(this.clock.getDelta()); - // this.update_animation_frame(); } this.light_holder.quaternion.copy(this.perspective_camera.quaternion); super.render(); if (this.animation && !this.animation.action.paused) { + this.update_animation_frame(); this.schedule_render(); } } - private nj_object_changed = (nj_data?: { nj_object: NjObject; has_skeleton: boolean }) => { + private nj_data_or_xvm_changed = () => { if (this.mesh) { this.scene.remove(this.mesh); this.mesh = undefined; @@ -88,13 +93,15 @@ export class ModelRenderer extends Renderer implements Disposable { this.animation = undefined; } + const nj_data = model_store.current_nj_data.get(); + if (nj_data) { const { nj_object, has_skeleton } = nj_data; let mesh: Mesh; - // TODO: - const textures: Texture[] | undefined = Math.random() > 1 ? [] : undefined; + const xvm = model_store.current_xvm.get(); + const textures = xvm ? xvm_to_textures(xvm) : undefined; const materials = textures && @@ -159,9 +166,6 @@ export class ModelRenderer extends Renderer implements Disposable { this.clock.start(); this.animation.action.play(); - // TODO: - // this.animation_playing = true; - // this.animation_frame_count = Math.round(PSO_FRAME_RATE * clip.duration) + 1; this.schedule_render(); }; @@ -172,13 +176,41 @@ export class ModelRenderer extends Renderer implements Disposable { } }; - private update(): void { - // 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(); - // } + private animation_playing_changed = (playing: boolean) => { + if (this.animation) { + this.animation.action.paused = !playing; - this.schedule_render(); + if (playing) { + this.clock.start(); + this.schedule_render(); + } else { + this.clock.stop(); + } + } + }; + + private animation_frame_rate_changed = (frame_rate: number) => { + if (this.animation) { + this.animation.mixer.timeScale = frame_rate / PSO_FRAME_RATE; + } + }; + + private animation_frame_changed = (frame: number) => { + const nj_motion = model_store.current_nj_motion.get(); + + if (this.animation && nj_motion) { + const frame_count = nj_motion.frame_count; + if (frame > frame_count) frame = 1; + if (frame < 1) frame = frame_count; + this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; + this.schedule_render(); + } + }; + + private update_animation_frame(): void { + if (this.animation && !this.animation.action.paused) { + const time = this.animation.action.time; + model_store.animation_frame.set(time * PSO_FRAME_RATE + 1); + } } } diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts index cb9baf05..cc952b5a 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/new/viewer/stores/ModelStore.ts @@ -4,24 +4,30 @@ import { NjMotion, parse_njm } from "../../../core/data_formats/parsing/ninja/mo import { NjObject, parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja"; import { CharacterClassModel } from "../domain/CharacterClassModel"; import { CharacterClassAnimation } from "../domain/CharacterClassAnimation"; -import { Property } from "../../core/observable/Property"; +import { WritableProperty } from "../../core/observable/WritableProperty"; import { get_character_class_animation_data, get_character_class_data, } from "../../../viewer/loading/character_class"; -import { Disposable } from "../../core/gui/Disposable"; +import { Disposable } from "../../core/observable/Disposable"; import { read_file } from "../../../core/read_file"; -import { create_animation_clip } from "../../../core/rendering/conversion/ninja_animation"; -import { parse_xvm } from "../../../core/data_formats/parsing/ninja/texture"; -import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; +import { property } from "../../core/observable"; +import { Property } from "../../core/observable/Property"; +import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; +import { parse_xvm, Xvm } from "../../../core/data_formats/parsing/ninja/texture"; import Logger = require("js-logger"); const logger = Logger.get("viewer/stores/ModelStore"); 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 { +export type NjData = { + nj_object: NjObject; + bone_count: number; + has_skeleton: boolean; +}; + +export class ModelStore implements Disposable { readonly models: CharacterClassModel[] = [ new CharacterClassModel("HUmar", 1, 10, new Set([6])), new CharacterClassModel("HUnewearl", 1, 10, new Set()), @@ -41,27 +47,29 @@ class ModelStore implements Disposable { .fill(undefined) .map((_, i) => new CharacterClassAnimation(i, `Animation ${i + 1}`)); - readonly current_model = new Property(undefined); + readonly current_model: WritableProperty = property(undefined); - readonly current_nj_data = new Property< - | { - nj_object: NjObject; - bone_count: number; - has_skeleton: boolean; - } - | undefined - >(undefined); + private readonly _current_nj_data = property(undefined); + readonly current_nj_data: Property = this._current_nj_data; - readonly current_animation = new Property(undefined); + private readonly _current_xvm = property(undefined); + readonly current_xvm: Property = this._current_xvm; - readonly current_nj_motion = new Property(undefined); + readonly show_skeleton: WritableProperty = property(false); - // @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 current_animation: WritableProperty = property( + undefined, + ); - readonly show_skeleton = new Property(false); + private readonly _current_nj_motion = property(undefined); + readonly current_nj_motion: Property = this._current_nj_motion; + + readonly animation_playing: WritableProperty = property(true); + readonly animation_frame_rate: WritableProperty = property(PSO_FRAME_RATE); + readonly animation_frame: WritableProperty = property(0); + readonly animation_frame_count: Property = this.current_nj_motion.map(njm => + njm ? njm.frame_count : 0, + ); private disposables: Disposable[] = []; @@ -76,23 +84,6 @@ class ModelStore implements Disposable { 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; - // } - // }; - // TODO: notify user of problems. load_file = async (file: File) => { try { @@ -101,40 +92,38 @@ class ModelStore implements Disposable { if (file.name.endsWith(".nj")) { this.current_model.set(undefined); - this.current_nj_data.set(undefined); const nj_object = parse_nj(cursor)[0]; - this.current_nj_data.set({ + this.set_current_nj_data({ nj_object, bone_count: nj_object.bone_count(), has_skeleton: true, }); } else if (file.name.endsWith(".xj")) { this.current_model.set(undefined); - this.current_nj_data.set(undefined); const nj_object = parse_xj(cursor)[0]; - this.current_nj_data.set({ + this.set_current_nj_data({ nj_object, bone_count: 0, has_skeleton: false, }); } else if (file.name.endsWith(".njm")) { this.current_animation.set(undefined); - this.current_nj_motion.set(undefined); + this._current_nj_motion.set(undefined); const nj_data = this.current_nj_data.get(); if (nj_data) { - this.current_nj_motion.set(parse_njm(cursor, nj_data.bone_count)); + this._current_nj_motion.set(parse_njm(cursor, nj_data.bone_count)); + } + } else if (file.name.endsWith(".xvm")) { + if (this.current_model) { + const xvm = parse_xvm(cursor); + this._current_xvm.set(xvm); } - // } 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}".`); } @@ -143,51 +132,30 @@ class ModelStore implements Disposable { } }; - // 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; - // } - // }; - private load_model = async (model?: CharacterClassModel) => { this.current_animation.set(undefined); if (model) { const nj_object = await this.get_nj_object(model); - this.current_nj_data.set({ + this.set_current_nj_data({ 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); + this._current_nj_data.set(undefined); } }; + private set_current_nj_data(nj_data: NjData): void { + this.current_model.set(undefined); + this._current_nj_data.set(undefined); + this._current_xvm.set(undefined); + this._current_nj_data.set(nj_data); + } + private async get_nj_object(model: CharacterClassModel): Promise { let nj_object = nj_object_cache.get(model.name); @@ -252,9 +220,10 @@ class ModelStore implements Disposable { const nj_data = this.current_nj_data.get(); if (nj_data && animation) { - this.current_nj_motion.set(await this.get_nj_motion(animation, nj_data.bone_count)); + this._current_nj_motion.set(await this.get_nj_motion(animation, nj_data.bone_count)); + this.animation_playing.set(true); } else { - this.current_nj_motion.set(undefined); + this._current_nj_motion.set(undefined); } }; @@ -275,10 +244,6 @@ class ModelStore implements Disposable { return nj_motion; } } - - // private set_textures = (textures: Texture[]) => { - // this.set_obj3d(textures); - // }; } export const model_store = new ModelStore(); From 66147fa5e1335aee1e354179a405f2fd9cb801bf Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 15:39:32 +0200 Subject: [PATCH 05/50] Fixed bug in model viewer. --- src/new/viewer/stores/ModelStore.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts index cc952b5a..b77768b5 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/new/viewer/stores/ModelStore.ts @@ -150,8 +150,6 @@ export class ModelStore implements Disposable { }; private set_current_nj_data(nj_data: NjData): void { - this.current_model.set(undefined); - this._current_nj_data.set(undefined); this._current_xvm.set(undefined); this._current_nj_data.set(nj_data); } From 060e746aac9bafcb3cb3853484ede56182d60fa7 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 16:55:12 +0200 Subject: [PATCH 06/50] Model viewer now pauses and unpauses when switching tools. --- src/new/viewer/stores/ModelStore.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/new/viewer/stores/ModelStore.ts b/src/new/viewer/stores/ModelStore.ts index b77768b5..0b288702 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/new/viewer/stores/ModelStore.ts @@ -15,6 +15,7 @@ import { property } from "../../core/observable"; import { Property } from "../../core/observable/Property"; import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; import { parse_xvm, Xvm } from "../../../core/data_formats/parsing/ninja/texture"; +import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import Logger = require("js-logger"); const logger = Logger.get("viewer/stores/ModelStore"); @@ -78,6 +79,19 @@ export class ModelStore implements Disposable { this.current_model.observe(this.load_model), this.current_animation.observe(this.load_animation), ); + + let prev_animation_playing = this.animation_playing.get(); + + this.disposables.push( + gui_store.tool.observe(tool => { + if (tool === GuiTool.Viewer) { + this.animation_playing.set(prev_animation_playing); + } else { + prev_animation_playing = this.animation_playing.get(); + this.animation_playing.set(false); + } + }), + ); } dispose(): void { From 72506461ab8e8302eae19952c08bc0c313d240ff Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 17:56:46 +0200 Subject: [PATCH 07/50] Swapped code using new and old UI system around. --- assets_generation/update_drops_ephinea.ts | 4 +- assets_generation/update_ephinea_data.ts | 4 +- .../application/gui/ApplicationView.ts | 0 .../application/gui/MainContentView.ts | 0 .../application/gui/NavigationView.css | 0 .../application/gui/NavigationView.ts | 0 src/application/stores/ApplicationStore.ts | 39 --- src/application/ui/ApplicationComponent.css | 48 --- src/application/ui/ApplicationComponent.tsx | 86 ----- src/core/data_formats/parsing/quest/bin.ts | 10 +- src/core/data_formats/parsing/quest/index.ts | 4 +- src/{new => }/core/gui/Button.css | 0 src/{new => }/core/gui/Button.ts | 0 src/{new => }/core/gui/CheckBox.ts | 0 src/{new => }/core/gui/Control.ts | 0 src/{new => }/core/gui/FileButton.css | 0 src/{new => }/core/gui/FileButton.ts | 0 src/{new => }/core/gui/Input.css | 0 src/{new => }/core/gui/Label.css | 0 src/{new => }/core/gui/Label.ts | 0 src/{new => }/core/gui/LabelledControl.ts | 0 src/{new => }/core/gui/LazyView.ts | 0 src/{new => }/core/gui/NumberInput.css | 0 src/{new => }/core/gui/NumberInput.ts | 4 +- src/{new => }/core/gui/RendererView.ts | 11 +- src/{new => }/core/gui/Resizable.ts | 0 src/{new => }/core/gui/ResizableView.ts | 0 src/{new => }/core/gui/TabContainer.css | 0 src/{new => }/core/gui/TabContainer.ts | 0 src/{new => }/core/gui/ToolBar.css | 0 src/{new => }/core/gui/ToolBar.ts | 0 src/{new => }/core/gui/View.ts | 0 src/{new => }/core/gui/dom.ts | 0 src/{new => }/core/observable/Disposable.ts | 0 src/{new => }/core/observable/Emitter.ts | 0 .../core/observable/MappedProperty.ts | 0 src/{new => }/core/observable/Observable.ts | 0 src/{new => }/core/observable/Property.ts | 6 +- .../core/observable/SimpleEmitter.ts | 0 .../core/observable/SimpleProperty.ts | 0 .../core/observable/WritableProperty.ts | 0 src/{new => }/core/observable/index.ts | 0 src/core/rendering/Renderer.ts | 1 + src/{new => }/core/stores/GuiStore.ts | 0 src/{new => }/index.css | 0 src/{new => }/index.ts | 26 +- src/index.tsx | 41 --- src/new/viewer/rendering/ModelRenderer.ts | 216 ------------ src/{ => old}/core/Loadable.ts | 0 src/{ => old}/core/domain/index.ts | 2 +- src/{ => old}/core/domain/items.ts | 0 src/{ => old}/core/dto.ts | 0 src/{ => old}/core/persistence.ts | 0 src/{ => old}/core/primitive_conversion.ts | 0 src/{ => old}/core/stores/ItemTypeStore.ts | 0 src/{ => old}/core/stores/ServerMap.ts | 5 +- src/{ => old}/core/ui/BigSelect.css | 0 src/{ => old}/core/ui/BigSelect.tsx | 0 src/{ => old}/core/ui/BigTable.css | 0 src/{ => old}/core/ui/BigTable.tsx | 0 .../core/ui/DisabledTextComponent.css | 0 .../core/ui/DisabledTextComponent.tsx | 0 src/{ => old}/core/ui/ErrorBoundary.css | 0 src/{ => old}/core/ui/ErrorBoundary.tsx | 0 src/{ => old}/core/ui/NumberInput.tsx | 0 src/{ => old}/core/ui/RendererComponent.tsx | 2 +- src/{ => old}/core/ui/SectionIdIcon.tsx | 0 src/{ => old}/core/ui/TextArea.tsx | 0 src/{ => old}/core/ui/TextInput.tsx | 0 src/{ => old}/core/ui/index.css | 0 src/{ => old}/core/ui/time.ts | 0 src/{ => old}/core/undo.test.ts | 0 src/{ => old}/core/undo.ts | 0 src/{ => old}/dps_calc/stores/DpsCalcStore.ts | 0 .../dps_calc/ui/DpsCalcComponent.tsx | 0 src/{ => old}/hunt_optimizer/domain/index.ts | 4 +- .../persistence/HuntMethodPersister.ts | 0 .../persistence/HuntOptimizerPersister.ts | 0 .../hunt_optimizer/stores/HuntMethodStore.ts | 2 +- .../stores/HuntOptimizerStore.ts | 15 +- .../hunt_optimizer/stores/ItemDropStore.ts | 2 +- .../ui/HuntOptimizerComponent.css | 0 .../ui/HuntOptimizerComponent.tsx | 0 .../hunt_optimizer/ui/MethodsComponent.css | 0 .../hunt_optimizer/ui/MethodsComponent.tsx | 4 +- .../ui/OptimizationResultComponent.css | 0 .../ui/OptimizationResultComponent.tsx | 2 +- .../hunt_optimizer/ui/OptimizerComponent.css | 0 .../hunt_optimizer/ui/OptimizerComponent.tsx | 0 .../ui/WantedItemsComponent.css | 0 .../ui/WantedItemsComponent.tsx | 0 .../quest_editor/domain/ObservableArea.ts | 0 .../domain/ObservableAreaVariant.ts | 0 .../quest_editor/domain/ObservableQuest.ts | 4 +- src/{ => old}/quest_editor/domain/Section.ts | 2 +- .../domain/observable_quest_entities.ts | 8 +- .../quest_editor/loading/LoadingCache.ts | 0 src/{ => old}/quest_editor/loading/areas.ts | 12 +- .../quest_editor/loading/entities.ts | 18 +- .../persistence/QuestEditorUiPersister.ts | 0 .../rendering/QuestEntityControls.ts | 2 +- .../rendering/QuestModelManager.ts | 0 .../quest_editor/rendering/QuestRenderer.ts | 2 +- .../rendering/conversion/areas.ts | 8 +- .../rendering/conversion/entities.ts | 6 +- .../scripting/AssemblyAnalyser.ts | 0 .../scripting/AssemblyLexer.test.ts | 0 .../quest_editor/scripting/AssemblyLexer.ts | 0 .../quest_editor/scripting/assembly.test.ts | 0 .../quest_editor/scripting/assembly.ts | 0 .../quest_editor/scripting/assembly_worker.ts | 0 .../scripting/assembly_worker_messages.ts | 0 .../ControlFlowGraph.test.ts | 0 .../data_flow_analysis/ControlFlowGraph.ts | 0 .../data_flow_analysis/ValueSet.test.ts | 0 .../scripting/data_flow_analysis/ValueSet.ts | 0 .../data_flow_analysis/register_value.test.ts | 0 .../data_flow_analysis/register_value.ts | 0 .../data_flow_analysis/stack_value.ts | 0 .../scripting/disassembly.test.ts | 10 +- .../quest_editor/scripting/disassembly.ts | 0 .../quest_editor/scripting/instructions.ts | 0 .../quest_editor/scripting/opcodes.ts | 0 .../quest_editor/scripting/vm/index.ts | 0 .../quest_editor/stores/AreaStore.ts | 4 +- .../quest_editor/stores/QuestEditorStore.ts | 43 ++- .../quest_editor/stores/quest_creation.ts | 8 +- .../quest_editor/ui/AddObjectComponent.tsx | 2 +- .../ui/AssemblyEditorComponent.css | 0 .../ui/AssemblyEditorComponent.tsx | 0 .../quest_editor/ui/EntityInfoComponent.css | 0 .../quest_editor/ui/EntityInfoComponent.tsx | 4 +- .../quest_editor/ui/NpcCountsComponent.css | 0 .../quest_editor/ui/NpcCountsComponent.tsx | 2 +- .../quest_editor/ui/QuestEditorComponent.css | 0 .../quest_editor/ui/QuestEditorComponent.tsx | 0 .../quest_editor/ui/QuestInfoComponent.css | 0 .../quest_editor/ui/QuestInfoComponent.tsx | 2 +- .../ui/QuestRendererComponent.tsx | 0 src/{ => old}/quest_editor/ui/Toolbar.css | 0 src/{ => old}/quest_editor/ui/Toolbar.tsx | 2 +- .../viewer/domain/CharacterClassAnimation.ts | 0 .../viewer/domain/CharacterClassModel.ts | 0 src/viewer/domain/index.ts | 12 - src/{new => }/viewer/gui/ModelView.css | 0 src/{new => }/viewer/gui/ModelView.ts | 15 +- src/{new => }/viewer/gui/TextureView.ts | 0 src/{new => }/viewer/gui/ViewerView.ts | 0 src/viewer/loading/character_class.ts | 6 +- src/viewer/rendering/ModelRenderer.ts | 232 +++++++++--- src/viewer/rendering/TextureRenderer.ts | 117 ------- src/{new => }/viewer/stores/ModelStore.ts | 36 +- src/viewer/stores/ModelViewerStore.ts | 329 ------------------ src/viewer/stores/TextureViewerStore.ts | 24 -- src/viewer/ui/ViewerComponent.css | 23 -- src/viewer/ui/ViewerComponent.tsx | 22 -- .../ui/models/AnimationSelectionComponent.css | 25 -- .../ui/models/AnimationSelectionComponent.tsx | 32 -- .../ui/models/ModelSelectionComponent.css | 15 - .../ui/models/ModelSelectionComponent.tsx | 43 --- src/viewer/ui/models/ModelViewerComponent.css | 35 -- src/viewer/ui/models/ModelViewerComponent.tsx | 116 ------ .../ui/textures/TextureViewerComponent.css | 14 - .../ui/textures/TextureViewerComponent.tsx | 60 ---- webpack.common.js | 2 +- 165 files changed, 347 insertions(+), 1488 deletions(-) rename src/{new => }/application/gui/ApplicationView.ts (100%) rename src/{new => }/application/gui/MainContentView.ts (100%) rename src/{new => }/application/gui/NavigationView.css (100%) rename src/{new => }/application/gui/NavigationView.ts (100%) delete mode 100644 src/application/stores/ApplicationStore.ts delete mode 100644 src/application/ui/ApplicationComponent.css delete mode 100644 src/application/ui/ApplicationComponent.tsx rename src/{new => }/core/gui/Button.css (100%) rename src/{new => }/core/gui/Button.ts (100%) rename src/{new => }/core/gui/CheckBox.ts (100%) rename src/{new => }/core/gui/Control.ts (100%) rename src/{new => }/core/gui/FileButton.css (100%) rename src/{new => }/core/gui/FileButton.ts (100%) rename src/{new => }/core/gui/Input.css (100%) rename src/{new => }/core/gui/Label.css (100%) rename src/{new => }/core/gui/Label.ts (100%) rename src/{new => }/core/gui/LabelledControl.ts (100%) rename src/{new => }/core/gui/LazyView.ts (100%) rename src/{new => }/core/gui/NumberInput.css (100%) rename src/{new => }/core/gui/NumberInput.ts (93%) rename src/{new => }/core/gui/RendererView.ts (71%) rename src/{new => }/core/gui/Resizable.ts (100%) rename src/{new => }/core/gui/ResizableView.ts (100%) rename src/{new => }/core/gui/TabContainer.css (100%) rename src/{new => }/core/gui/TabContainer.ts (100%) rename src/{new => }/core/gui/ToolBar.css (100%) rename src/{new => }/core/gui/ToolBar.ts (100%) rename src/{new => }/core/gui/View.ts (100%) rename src/{new => }/core/gui/dom.ts (100%) rename src/{new => }/core/observable/Disposable.ts (100%) rename src/{new => }/core/observable/Emitter.ts (100%) rename src/{new => }/core/observable/MappedProperty.ts (100%) rename src/{new => }/core/observable/Observable.ts (100%) rename src/{new => }/core/observable/Property.ts (58%) rename src/{new => }/core/observable/SimpleEmitter.ts (100%) rename src/{new => }/core/observable/SimpleProperty.ts (100%) rename src/{new => }/core/observable/WritableProperty.ts (100%) rename src/{new => }/core/observable/index.ts (100%) rename src/{new => }/core/stores/GuiStore.ts (100%) rename src/{new => }/index.css (100%) rename src/{new => }/index.ts (53%) delete mode 100644 src/index.tsx delete mode 100644 src/new/viewer/rendering/ModelRenderer.ts rename src/{ => old}/core/Loadable.ts (100%) rename src/{ => old}/core/domain/index.ts (91%) rename src/{ => old}/core/domain/items.ts (100%) rename src/{ => old}/core/dto.ts (100%) rename src/{ => old}/core/persistence.ts (100%) rename src/{ => old}/core/primitive_conversion.ts (100%) rename src/{ => old}/core/stores/ItemTypeStore.ts (100%) rename src/{ => old}/core/stores/ServerMap.ts (70%) rename src/{ => old}/core/ui/BigSelect.css (100%) rename src/{ => old}/core/ui/BigSelect.tsx (100%) rename src/{ => old}/core/ui/BigTable.css (100%) rename src/{ => old}/core/ui/BigTable.tsx (100%) rename src/{ => old}/core/ui/DisabledTextComponent.css (100%) rename src/{ => old}/core/ui/DisabledTextComponent.tsx (100%) rename src/{ => old}/core/ui/ErrorBoundary.css (100%) rename src/{ => old}/core/ui/ErrorBoundary.tsx (100%) rename src/{ => old}/core/ui/NumberInput.tsx (100%) rename src/{ => old}/core/ui/RendererComponent.tsx (95%) rename src/{ => old}/core/ui/SectionIdIcon.tsx (100%) rename src/{ => old}/core/ui/TextArea.tsx (100%) rename src/{ => old}/core/ui/TextInput.tsx (100%) rename src/{ => old}/core/ui/index.css (100%) rename src/{ => old}/core/ui/time.ts (100%) rename src/{ => old}/core/undo.test.ts (100%) rename src/{ => old}/core/undo.ts (100%) rename src/{ => old}/dps_calc/stores/DpsCalcStore.ts (100%) rename src/{ => old}/dps_calc/ui/DpsCalcComponent.tsx (100%) rename src/{ => old}/hunt_optimizer/domain/index.ts (93%) rename src/{ => old}/hunt_optimizer/persistence/HuntMethodPersister.ts (100%) rename src/{ => old}/hunt_optimizer/persistence/HuntOptimizerPersister.ts (100%) rename src/{ => old}/hunt_optimizer/stores/HuntMethodStore.ts (97%) rename src/{ => old}/hunt_optimizer/stores/HuntOptimizerStore.ts (95%) rename src/{ => old}/hunt_optimizer/stores/ItemDropStore.ts (97%) rename src/{ => old}/hunt_optimizer/ui/HuntOptimizerComponent.css (100%) rename src/{ => old}/hunt_optimizer/ui/HuntOptimizerComponent.tsx (100%) rename src/{ => old}/hunt_optimizer/ui/MethodsComponent.css (100%) rename src/{ => old}/hunt_optimizer/ui/MethodsComponent.tsx (97%) rename src/{ => old}/hunt_optimizer/ui/OptimizationResultComponent.css (100%) rename src/{ => old}/hunt_optimizer/ui/OptimizationResultComponent.tsx (98%) rename src/{ => old}/hunt_optimizer/ui/OptimizerComponent.css (100%) rename src/{ => old}/hunt_optimizer/ui/OptimizerComponent.tsx (100%) rename src/{ => old}/hunt_optimizer/ui/WantedItemsComponent.css (100%) rename src/{ => old}/hunt_optimizer/ui/WantedItemsComponent.tsx (100%) rename src/{ => old}/quest_editor/domain/ObservableArea.ts (100%) rename src/{ => old}/quest_editor/domain/ObservableAreaVariant.ts (100%) rename src/{ => old}/quest_editor/domain/ObservableQuest.ts (97%) rename src/{ => old}/quest_editor/domain/Section.ts (93%) rename src/{ => old}/quest_editor/domain/observable_quest_entities.ts (94%) rename src/{ => old}/quest_editor/loading/LoadingCache.ts (100%) rename src/{ => old}/quest_editor/loading/areas.ts (91%) rename src/{ => old}/quest_editor/loading/entities.ts (92%) rename src/{ => old}/quest_editor/persistence/QuestEditorUiPersister.ts (100%) rename src/{ => old}/quest_editor/rendering/QuestEntityControls.ts (99%) rename src/{ => old}/quest_editor/rendering/QuestModelManager.ts (100%) rename src/{ => old}/quest_editor/rendering/QuestRenderer.ts (98%) rename src/{ => old}/quest_editor/rendering/conversion/areas.ts (91%) rename src/{ => old}/quest_editor/rendering/conversion/entities.ts (89%) rename src/{ => old}/quest_editor/scripting/AssemblyAnalyser.ts (100%) rename src/{ => old}/quest_editor/scripting/AssemblyLexer.test.ts (100%) rename src/{ => old}/quest_editor/scripting/AssemblyLexer.ts (100%) rename src/{ => old}/quest_editor/scripting/assembly.test.ts (100%) rename src/{ => old}/quest_editor/scripting/assembly.ts (100%) rename src/{ => old}/quest_editor/scripting/assembly_worker.ts (100%) rename src/{ => old}/quest_editor/scripting/assembly_worker_messages.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/ValueSet.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/register_value.test.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/register_value.ts (100%) rename src/{ => old}/quest_editor/scripting/data_flow_analysis/stack_value.ts (100%) rename src/{ => old}/quest_editor/scripting/disassembly.test.ts (83%) rename src/{ => old}/quest_editor/scripting/disassembly.ts (100%) rename src/{ => old}/quest_editor/scripting/instructions.ts (100%) rename src/{ => old}/quest_editor/scripting/opcodes.ts (100%) rename src/{ => old}/quest_editor/scripting/vm/index.ts (100%) rename src/{ => old}/quest_editor/stores/AreaStore.ts (91%) rename src/{ => old}/quest_editor/stores/QuestEditorStore.ts (90%) rename src/{ => old}/quest_editor/stores/quest_creation.ts (98%) rename src/{ => old}/quest_editor/ui/AddObjectComponent.tsx (94%) rename src/{ => old}/quest_editor/ui/AssemblyEditorComponent.css (100%) rename src/{ => old}/quest_editor/ui/AssemblyEditorComponent.tsx (100%) rename src/{ => old}/quest_editor/ui/EntityInfoComponent.css (100%) rename src/{ => old}/quest_editor/ui/EntityInfoComponent.tsx (96%) rename src/{ => old}/quest_editor/ui/NpcCountsComponent.css (100%) rename src/{ => old}/quest_editor/ui/NpcCountsComponent.tsx (93%) rename src/{ => old}/quest_editor/ui/QuestEditorComponent.css (100%) rename src/{ => old}/quest_editor/ui/QuestEditorComponent.tsx (100%) rename src/{ => old}/quest_editor/ui/QuestInfoComponent.css (100%) rename src/{ => old}/quest_editor/ui/QuestInfoComponent.tsx (98%) rename src/{ => old}/quest_editor/ui/QuestRendererComponent.tsx (100%) rename src/{ => old}/quest_editor/ui/Toolbar.css (100%) rename src/{ => old}/quest_editor/ui/Toolbar.tsx (98%) rename src/{new => }/viewer/domain/CharacterClassAnimation.ts (100%) rename src/{new => }/viewer/domain/CharacterClassModel.ts (100%) delete mode 100644 src/viewer/domain/index.ts rename src/{new => }/viewer/gui/ModelView.css (100%) rename src/{new => }/viewer/gui/ModelView.ts (93%) rename src/{new => }/viewer/gui/TextureView.ts (100%) rename src/{new => }/viewer/gui/ViewerView.ts (100%) delete mode 100644 src/viewer/rendering/TextureRenderer.ts rename src/{new => }/viewer/stores/ModelStore.ts (88%) delete mode 100644 src/viewer/stores/ModelViewerStore.ts delete mode 100644 src/viewer/stores/TextureViewerStore.ts delete mode 100644 src/viewer/ui/ViewerComponent.css delete mode 100644 src/viewer/ui/ViewerComponent.tsx delete mode 100644 src/viewer/ui/models/AnimationSelectionComponent.css delete mode 100644 src/viewer/ui/models/AnimationSelectionComponent.tsx delete mode 100644 src/viewer/ui/models/ModelSelectionComponent.css delete mode 100644 src/viewer/ui/models/ModelSelectionComponent.tsx delete mode 100644 src/viewer/ui/models/ModelViewerComponent.css delete mode 100644 src/viewer/ui/models/ModelViewerComponent.tsx delete mode 100644 src/viewer/ui/textures/TextureViewerComponent.css delete mode 100644 src/viewer/ui/textures/TextureViewerComponent.tsx diff --git a/assets_generation/update_drops_ephinea.ts b/assets_generation/update_drops_ephinea.ts index a093bf5c..c2cc003e 100644 --- a/assets_generation/update_drops_ephinea.ts +++ b/assets_generation/update_drops_ephinea.ts @@ -3,8 +3,8 @@ import { writeFileSync } from "fs"; import "isomorphic-fetch"; import Logger from "js-logger"; import { ASSETS_DIR } from "."; -import { Difficulty, SectionId, SectionIds } from "../src/core/domain"; -import { BoxDropDto, EnemyDropDto, ItemTypeDto } from "../src/core/dto"; +import { Difficulty, SectionId, SectionIds } from "../src/old/core/domain"; +import { BoxDropDto, EnemyDropDto, ItemTypeDto } from "../src/old/core/dto"; import { name_and_episode_to_npc_type, NpcType, diff --git a/assets_generation/update_ephinea_data.ts b/assets_generation/update_ephinea_data.ts index 5c11aee5..a3021317 100644 --- a/assets_generation/update_ephinea_data.ts +++ b/assets_generation/update_ephinea_data.ts @@ -5,8 +5,8 @@ import { BufferCursor } from "../src/core/data_formats/cursor/BufferCursor"; import { ItemPmt, parse_item_pmt } from "../src/core/data_formats/parsing/itempmt"; import { parse_quest } from "../src/core/data_formats/parsing/quest"; import { parse_unitxt, Unitxt } from "../src/core/data_formats/parsing/unitxt"; -import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/core/domain"; -import { BoxDropDto, EnemyDropDto, ItemTypeDto, QuestDto } from "../src/core/dto"; +import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/old/core/domain"; +import { BoxDropDto, EnemyDropDto, ItemTypeDto, QuestDto } from "../src/old/core/dto"; import { update_drops_from_website } from "./update_drops_ephinea"; import { Episode, EPISODES } from "../src/core/data_formats/parsing/quest/Episode"; import { npc_data, NPC_TYPES, NpcType } from "../src/core/data_formats/parsing/quest/npc_types"; diff --git a/src/new/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts similarity index 100% rename from src/new/application/gui/ApplicationView.ts rename to src/application/gui/ApplicationView.ts diff --git a/src/new/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts similarity index 100% rename from src/new/application/gui/MainContentView.ts rename to src/application/gui/MainContentView.ts diff --git a/src/new/application/gui/NavigationView.css b/src/application/gui/NavigationView.css similarity index 100% rename from src/new/application/gui/NavigationView.css rename to src/application/gui/NavigationView.css diff --git a/src/new/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts similarity index 100% rename from src/new/application/gui/NavigationView.ts rename to src/application/gui/NavigationView.ts diff --git a/src/application/stores/ApplicationStore.ts b/src/application/stores/ApplicationStore.ts deleted file mode 100644 index 7b2e7b78..00000000 --- a/src/application/stores/ApplicationStore.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { autorun, observable } from "mobx"; -import { Server } from "../../core/domain"; - -class ApplicationStore { - @observable current_server: Server = Server.Ephinea; - @observable current_tool: string = this.init_tool(); - - private global_keyup_handlers = new Map void>(); - - constructor() { - autorun(() => { - window.location.hash = `#/${this.current_tool}`; - }); - } - - on_global_keyup(tool: string, binding: string, handler: () => void): void { - this.global_keyup_handlers.set(`${tool} -> ${binding}`, handler); - } - - dispatch_global_keyup = (e: KeyboardEvent) => { - const binding_parts: string[] = []; - if (e.ctrlKey) binding_parts.push("Ctrl"); - if (e.shiftKey) binding_parts.push("Shift"); - if (e.altKey) binding_parts.push("Alt"); - binding_parts.push(e.key.toUpperCase()); - - const binding = binding_parts.join("-"); - - const handler = this.global_keyup_handlers.get(`${this.current_tool} -> ${binding}`); - if (handler) handler(); - }; - - private init_tool(): string { - const tool = window.location.hash.slice(2); - return tool.length ? tool : "viewer"; - } -} - -export const application_store = new ApplicationStore(); diff --git a/src/application/ui/ApplicationComponent.css b/src/application/ui/ApplicationComponent.css deleted file mode 100644 index c8f835f4..00000000 --- a/src/application/ui/ApplicationComponent.css +++ /dev/null @@ -1,48 +0,0 @@ -.main { - display: flex; - flex-direction: column; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - -.navbar { - display: flex; - border-bottom: solid 1px var(--border-color-split); -} - -.heading_menu { - flex: 1; - margin-bottom: -1px !important; -} - -.server_select { - display: flex; - align-items: center; - margin: 0 6px; -} - -.server_select > span { - display: inline-block; - margin-right: 10px; -} - -.beta { - color: #f55656; - font-weight: bold; -} - -.content { - flex: 1; - display: flex; - flex-direction: column; - align-items: stretch; - overflow: hidden; -} - -.content > * { - flex: 1; - overflow: hidden; -} diff --git a/src/application/ui/ApplicationComponent.tsx b/src/application/ui/ApplicationComponent.tsx deleted file mode 100644 index df858c2f..00000000 --- a/src/application/ui/ApplicationComponent.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Menu, Select } from "antd"; -import { ClickParam } from "antd/lib/menu"; -import { observer } from "mobx-react"; -import React, { ReactNode, Component } from "react"; -import { Server } from "../../core/domain"; -import styles from "./ApplicationComponent.css"; -import { DpsCalcComponent } from "../../dps_calc/ui/DpsCalcComponent"; -import { with_error_boundary } from "../../core/ui/ErrorBoundary"; -import { HuntOptimizerComponent } from "../../hunt_optimizer/ui/HuntOptimizerComponent"; -import { QuestEditorComponent } from "../../quest_editor/ui/QuestEditorComponent"; -import { ViewerComponent } from "../../viewer/ui/ViewerComponent"; -import { application_store } from "../stores/ApplicationStore"; - -const Viewer = with_error_boundary(ViewerComponent); -const QuestEditor = with_error_boundary(QuestEditorComponent); -const HuntOptimizer = with_error_boundary(HuntOptimizerComponent); -const DpsCalc = with_error_boundary(DpsCalcComponent); - -@observer -export class ApplicationComponent extends Component { - componentDidMount(): void { - window.addEventListener("keyup", this.keyup); - } - - componentWillUnmount(): void { - window.removeEventListener("keyup", this.keyup); - } - - render(): ReactNode { - let tool_component; - - switch (application_store.current_tool) { - case "viewer": - tool_component = ; - break; - case "quest_editor": - tool_component = ; - break; - case "hunt_optimizer": - tool_component = ; - break; - case "dps_calc": - tool_component = ; - break; - } - - return ( -
-
- - - Viewer(Beta) - - - Quest Editor(Beta) - - Hunt Optimizer - {/* - DPS Calculator - */} - -
- Server: - -
-
-
{tool_component}
-
- ); - } - - private menu_clicked = (e: ClickParam) => { - application_store.current_tool = e.key; - }; - - private keyup = (e: KeyboardEvent) => { - application_store.dispatch_global_keyup(e); - }; -} diff --git a/src/core/data_formats/parsing/quest/bin.ts b/src/core/data_formats/parsing/quest/bin.ts index 50950a7a..117f4ff3 100644 --- a/src/core/data_formats/parsing/quest/bin.ts +++ b/src/core/data_formats/parsing/quest/bin.ts @@ -1,8 +1,8 @@ import Logger from "js-logger"; import { Endianness } from "../../Endianness"; -import { ControlFlowGraph } from "../../../../quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; -import { register_value } from "../../../../quest_editor/scripting/data_flow_analysis/register_value"; -import { stack_value } from "../../../../quest_editor/scripting/data_flow_analysis/stack_value"; +import { ControlFlowGraph } from "../../../../old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; +import { register_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/register_value"; +import { stack_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/stack_value"; import { Arg, DataSegment, @@ -11,13 +11,13 @@ import { Segment, SegmentType, StringSegment, -} from "../../../../quest_editor/scripting/instructions"; +} from "../../../../old/quest_editor/scripting/instructions"; import { Kind, Opcode, OPCODES, StackInteraction, -} from "../../../../quest_editor/scripting/opcodes"; +} from "../../../../old/quest_editor/scripting/opcodes"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { Cursor } from "../../cursor/Cursor"; import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 6591f7ab..49f20ffa 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -4,8 +4,8 @@ import { InstructionSegment, Segment, SegmentType, -} from "../../../../quest_editor/scripting/instructions"; -import { Opcode } from "../../../../quest_editor/scripting/opcodes"; +} from "../../../../old/quest_editor/scripting/instructions"; +import { Opcode } from "../../../../old/quest_editor/scripting/opcodes"; import { prs_compress } from "../../compression/prs/compress"; import { prs_decompress } from "../../compression/prs/decompress"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; diff --git a/src/new/core/gui/Button.css b/src/core/gui/Button.css similarity index 100% rename from src/new/core/gui/Button.css rename to src/core/gui/Button.css diff --git a/src/new/core/gui/Button.ts b/src/core/gui/Button.ts similarity index 100% rename from src/new/core/gui/Button.ts rename to src/core/gui/Button.ts diff --git a/src/new/core/gui/CheckBox.ts b/src/core/gui/CheckBox.ts similarity index 100% rename from src/new/core/gui/CheckBox.ts rename to src/core/gui/CheckBox.ts diff --git a/src/new/core/gui/Control.ts b/src/core/gui/Control.ts similarity index 100% rename from src/new/core/gui/Control.ts rename to src/core/gui/Control.ts diff --git a/src/new/core/gui/FileButton.css b/src/core/gui/FileButton.css similarity index 100% rename from src/new/core/gui/FileButton.css rename to src/core/gui/FileButton.css diff --git a/src/new/core/gui/FileButton.ts b/src/core/gui/FileButton.ts similarity index 100% rename from src/new/core/gui/FileButton.ts rename to src/core/gui/FileButton.ts diff --git a/src/new/core/gui/Input.css b/src/core/gui/Input.css similarity index 100% rename from src/new/core/gui/Input.css rename to src/core/gui/Input.css diff --git a/src/new/core/gui/Label.css b/src/core/gui/Label.css similarity index 100% rename from src/new/core/gui/Label.css rename to src/core/gui/Label.css diff --git a/src/new/core/gui/Label.ts b/src/core/gui/Label.ts similarity index 100% rename from src/new/core/gui/Label.ts rename to src/core/gui/Label.ts diff --git a/src/new/core/gui/LabelledControl.ts b/src/core/gui/LabelledControl.ts similarity index 100% rename from src/new/core/gui/LabelledControl.ts rename to src/core/gui/LabelledControl.ts diff --git a/src/new/core/gui/LazyView.ts b/src/core/gui/LazyView.ts similarity index 100% rename from src/new/core/gui/LazyView.ts rename to src/core/gui/LazyView.ts diff --git a/src/new/core/gui/NumberInput.css b/src/core/gui/NumberInput.css similarity index 100% rename from src/new/core/gui/NumberInput.css rename to src/core/gui/NumberInput.css diff --git a/src/new/core/gui/NumberInput.ts b/src/core/gui/NumberInput.ts similarity index 93% rename from src/new/core/gui/NumberInput.ts rename to src/core/gui/NumberInput.ts index deb9c2a0..347232ea 100644 --- a/src/new/core/gui/NumberInput.ts +++ b/src/core/gui/NumberInput.ts @@ -4,7 +4,7 @@ import { create_el } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import { property } from "../observable"; import { LabelledControl } from "./LabelledControl"; -import { is_property, Property } from "../observable/Property"; +import { is_any_property, Property } from "../observable/Property"; export class NumberInput extends LabelledControl { readonly element: HTMLInputElement = create_el("input", "core_NumberInput core_Input"); @@ -40,7 +40,7 @@ export class NumberInput extends LabelledControl { } private set_prop(prop: "min" | "max" | "step", value: T | Property): void { - if (is_property(value)) { + if (is_any_property(value)) { this.element[prop] = String(value.get()); this.disposable(value.observe(v => (this.element[prop] = String(v)))); } else { diff --git a/src/new/core/gui/RendererView.ts b/src/core/gui/RendererView.ts similarity index 71% rename from src/new/core/gui/RendererView.ts rename to src/core/gui/RendererView.ts index 2f685279..2756fb00 100644 --- a/src/new/core/gui/RendererView.ts +++ b/src/core/gui/RendererView.ts @@ -1,6 +1,6 @@ import { ResizableView } from "./ResizableView"; import { create_el } from "./dom"; -import { Renderer } from "../../../core/rendering/Renderer"; +import { Renderer } from "../rendering/Renderer"; export class RendererView extends ResizableView { readonly element = create_el("div"); @@ -11,9 +11,14 @@ export class RendererView extends ResizableView { this.element.append(renderer.dom_element); this.disposable(renderer); + } - // TODO: stop on hidden - renderer.start_rendering(); + start_rendering(): void { + this.renderer.start_rendering(); + } + + stop_rendering(): void { + this.renderer.stop_rendering(); } resize(width: number, height: number): this { diff --git a/src/new/core/gui/Resizable.ts b/src/core/gui/Resizable.ts similarity index 100% rename from src/new/core/gui/Resizable.ts rename to src/core/gui/Resizable.ts diff --git a/src/new/core/gui/ResizableView.ts b/src/core/gui/ResizableView.ts similarity index 100% rename from src/new/core/gui/ResizableView.ts rename to src/core/gui/ResizableView.ts diff --git a/src/new/core/gui/TabContainer.css b/src/core/gui/TabContainer.css similarity index 100% rename from src/new/core/gui/TabContainer.css rename to src/core/gui/TabContainer.css diff --git a/src/new/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts similarity index 100% rename from src/new/core/gui/TabContainer.ts rename to src/core/gui/TabContainer.ts diff --git a/src/new/core/gui/ToolBar.css b/src/core/gui/ToolBar.css similarity index 100% rename from src/new/core/gui/ToolBar.css rename to src/core/gui/ToolBar.css diff --git a/src/new/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts similarity index 100% rename from src/new/core/gui/ToolBar.ts rename to src/core/gui/ToolBar.ts diff --git a/src/new/core/gui/View.ts b/src/core/gui/View.ts similarity index 100% rename from src/new/core/gui/View.ts rename to src/core/gui/View.ts diff --git a/src/new/core/gui/dom.ts b/src/core/gui/dom.ts similarity index 100% rename from src/new/core/gui/dom.ts rename to src/core/gui/dom.ts diff --git a/src/new/core/observable/Disposable.ts b/src/core/observable/Disposable.ts similarity index 100% rename from src/new/core/observable/Disposable.ts rename to src/core/observable/Disposable.ts diff --git a/src/new/core/observable/Emitter.ts b/src/core/observable/Emitter.ts similarity index 100% rename from src/new/core/observable/Emitter.ts rename to src/core/observable/Emitter.ts diff --git a/src/new/core/observable/MappedProperty.ts b/src/core/observable/MappedProperty.ts similarity index 100% rename from src/new/core/observable/MappedProperty.ts rename to src/core/observable/MappedProperty.ts diff --git a/src/new/core/observable/Observable.ts b/src/core/observable/Observable.ts similarity index 100% rename from src/new/core/observable/Observable.ts rename to src/core/observable/Observable.ts diff --git a/src/new/core/observable/Property.ts b/src/core/observable/Property.ts similarity index 58% rename from src/new/core/observable/Property.ts rename to src/core/observable/Property.ts index 093be6dc..70ef2889 100644 --- a/src/new/core/observable/Property.ts +++ b/src/core/observable/Property.ts @@ -10,6 +10,10 @@ export interface Property extends Observable> { export type PropertyMeta = { old_value: T }; -export function is_property(observable: any): observable is Property { +export function is_property(observable: Observable): observable is Property { + return (observable as any).is_property; +} + +export function is_any_property(observable: any): observable is Property { return (observable as any).is_property; } diff --git a/src/new/core/observable/SimpleEmitter.ts b/src/core/observable/SimpleEmitter.ts similarity index 100% rename from src/new/core/observable/SimpleEmitter.ts rename to src/core/observable/SimpleEmitter.ts diff --git a/src/new/core/observable/SimpleProperty.ts b/src/core/observable/SimpleProperty.ts similarity index 100% rename from src/new/core/observable/SimpleProperty.ts rename to src/core/observable/SimpleProperty.ts diff --git a/src/new/core/observable/WritableProperty.ts b/src/core/observable/WritableProperty.ts similarity index 100% rename from src/new/core/observable/WritableProperty.ts rename to src/core/observable/WritableProperty.ts diff --git a/src/new/core/observable/index.ts b/src/core/observable/index.ts similarity index 100% rename from src/new/core/observable/index.ts rename to src/core/observable/index.ts diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 20700723..f0964b03 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -79,6 +79,7 @@ export abstract class Renderer { } start_rendering(): void { + this.schedule_render(); requestAnimationFrame(this.call_render); } diff --git a/src/new/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts similarity index 100% rename from src/new/core/stores/GuiStore.ts rename to src/core/stores/GuiStore.ts diff --git a/src/new/index.css b/src/index.css similarity index 100% rename from src/new/index.css rename to src/index.css diff --git a/src/new/index.ts b/src/index.ts similarity index 53% rename from src/new/index.ts rename to src/index.ts index 31788a71..1bd1e9b8 100644 --- a/src/new/index.ts +++ b/src/index.ts @@ -2,8 +2,30 @@ import { ApplicationView } from "./application/gui/ApplicationView"; import { Disposable } from "./core/observable/Disposable"; import "./index.css"; import { throttle } from "lodash"; +import Logger from "js-logger"; -export function initialize(): Disposable { +Logger.useDefaults({ + defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], +}); + +// Disable native undo/redo. +document.addEventListener("keydown", e => { + const kbe = e as KeyboardEvent; + + if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") { + kbe.preventDefault(); + } +}); +// This doesn't work in FireFox: +document.addEventListener("beforeinput", e => { + const ie = e as any; + + if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") { + e.preventDefault(); + } +}); + +function initialize(): Disposable { const application_view = new ApplicationView(); const resize = throttle( @@ -25,3 +47,5 @@ export function initialize(): Disposable { }, }; } + +initialize(); diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 2fa9c9e7..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -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 "golden-layout/src/css/goldenlayout-base.css"; -import "golden-layout/src/css/goldenlayout-dark-theme.css"; -// import "antd/dist/antd.less"; -import { initialize } from "./new"; - -Logger.useDefaults({ - defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], -}); - -// Disable native undo/redo. -document.addEventListener("keydown", e => { - const kbe = e as KeyboardEvent; - - if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") { - kbe.preventDefault(); - } -}); -// This doesn't work in FireFox: -document.addEventListener("beforeinput", e => { - const ie = e as any; - - if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") { - e.preventDefault(); - } -}); - -// const root_element = document.createElement("div"); -// root_element.id = styles.phantasmal_world_root; -// document.body.append(root_element); -// -// ReactDOM.render(, root_element); - -initialize(); diff --git a/src/new/viewer/rendering/ModelRenderer.ts b/src/new/viewer/rendering/ModelRenderer.ts deleted file mode 100644 index 10c428ad..00000000 --- a/src/new/viewer/rendering/ModelRenderer.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { - AnimationAction, - AnimationClip, - AnimationMixer, - Clock, - DoubleSide, - Mesh, - MeshLambertMaterial, - Object3D, - PerspectiveCamera, - SkeletonHelper, - SkinnedMesh, - Texture, - Vector3, -} from "three"; -import { Renderer } from "../../../core/rendering/Renderer"; -import { model_store } from "../stores/ModelStore"; -import { Disposable } from "../../core/observable/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"; -import { - create_animation_clip, - PSO_FRAME_RATE, -} from "../../../core/rendering/conversion/ninja_animation"; -import { NjMotion } from "../../../core/data_formats/parsing/ninja/motion"; -import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; - -export class ModelRenderer extends Renderer implements Disposable { - private readonly perspective_camera: PerspectiveCamera; - private readonly disposables: Disposable[] = []; - private readonly clock = new Clock(); - private mesh?: Object3D; - private skeleton_helper?: SkeletonHelper; - private animation?: { - mixer: AnimationMixer; - clip: AnimationClip; - action: AnimationAction; - }; - - 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.nj_data_or_xvm_changed), - model_store.current_xvm.observe(this.nj_data_or_xvm_changed), - model_store.current_nj_motion.observe(this.nj_motion_changed), - model_store.show_skeleton.observe(this.show_skeleton_changed), - model_store.animation_playing.observe(this.animation_playing_changed), - model_store.animation_frame_rate.observe(this.animation_frame_rate_changed), - model_store.animation_frame.observe(this.animation_frame_changed), - ); - } - - 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 (this.animation) { - this.animation.mixer.update(this.clock.getDelta()); - } - - this.light_holder.quaternion.copy(this.perspective_camera.quaternion); - super.render(); - - if (this.animation && !this.animation.action.paused) { - this.update_animation_frame(); - this.schedule_render(); - } - } - - private nj_data_or_xvm_changed = () => { - if (this.mesh) { - this.scene.remove(this.mesh); - this.mesh = undefined; - this.scene.remove(this.skeleton_helper!); - this.skeleton_helper = undefined; - } - - if (this.animation) { - this.animation.mixer.stopAllAction(); - if (this.mesh) this.animation.mixer.uncacheRoot(this.mesh); - this.animation = undefined; - } - - const nj_data = model_store.current_nj_data.get(); - - if (nj_data) { - const { nj_object, has_skeleton } = nj_data; - - let mesh: Mesh; - - const xvm = model_store.current_xvm.get(); - const textures = xvm ? xvm_to_textures(xvm) : undefined; - - 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.mesh = mesh; - this.scene.add(mesh); - - this.skeleton_helper = new SkeletonHelper(mesh); - 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.schedule_render(); - }; - - private nj_motion_changed = (nj_motion?: NjMotion) => { - let mixer!: AnimationMixer; - - if (this.animation) { - this.animation.mixer.stopAllAction(); - mixer = this.animation.mixer; - } - - const nj_data = model_store.current_nj_data.get(); - - if (!this.mesh || !(this.mesh instanceof SkinnedMesh) || !nj_motion || !nj_data) return; - - if (!this.animation) { - mixer = new AnimationMixer(this.mesh); - } - - const clip = create_animation_clip(nj_data.nj_object, nj_motion); - - this.animation = { - mixer, - clip, - action: mixer.clipAction(clip), - }; - - this.clock.start(); - this.animation.action.play(); - this.schedule_render(); - }; - - private show_skeleton_changed = (show_skeleton: boolean) => { - if (this.skeleton_helper) { - this.skeleton_helper.visible = show_skeleton; - this.schedule_render(); - } - }; - - private animation_playing_changed = (playing: boolean) => { - if (this.animation) { - this.animation.action.paused = !playing; - - if (playing) { - this.clock.start(); - this.schedule_render(); - } else { - this.clock.stop(); - } - } - }; - - private animation_frame_rate_changed = (frame_rate: number) => { - if (this.animation) { - this.animation.mixer.timeScale = frame_rate / PSO_FRAME_RATE; - } - }; - - private animation_frame_changed = (frame: number) => { - const nj_motion = model_store.current_nj_motion.get(); - - if (this.animation && nj_motion) { - const frame_count = nj_motion.frame_count; - if (frame > frame_count) frame = 1; - if (frame < 1) frame = frame_count; - this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; - this.schedule_render(); - } - }; - - private update_animation_frame(): void { - if (this.animation && !this.animation.action.paused) { - const time = this.animation.action.time; - model_store.animation_frame.set(time * PSO_FRAME_RATE + 1); - } - } -} diff --git a/src/core/Loadable.ts b/src/old/core/Loadable.ts similarity index 100% rename from src/core/Loadable.ts rename to src/old/core/Loadable.ts diff --git a/src/core/domain/index.ts b/src/old/core/domain/index.ts similarity index 91% rename from src/core/domain/index.ts rename to src/old/core/domain/index.ts index 804a058c..517ae122 100644 --- a/src/core/domain/index.ts +++ b/src/old/core/domain/index.ts @@ -1,4 +1,4 @@ -import { enum_values } from "../enums"; +import { enum_values } from "../../../core/enums"; export const RARE_ENEMY_PROB = 1 / 512; export const KONDRIEU_PROB = 1 / 10; diff --git a/src/core/domain/items.ts b/src/old/core/domain/items.ts similarity index 100% rename from src/core/domain/items.ts rename to src/old/core/domain/items.ts diff --git a/src/core/dto.ts b/src/old/core/dto.ts similarity index 100% rename from src/core/dto.ts rename to src/old/core/dto.ts diff --git a/src/core/persistence.ts b/src/old/core/persistence.ts similarity index 100% rename from src/core/persistence.ts rename to src/old/core/persistence.ts diff --git a/src/core/primitive_conversion.ts b/src/old/core/primitive_conversion.ts similarity index 100% rename from src/core/primitive_conversion.ts rename to src/old/core/primitive_conversion.ts diff --git a/src/core/stores/ItemTypeStore.ts b/src/old/core/stores/ItemTypeStore.ts similarity index 100% rename from src/core/stores/ItemTypeStore.ts rename to src/old/core/stores/ItemTypeStore.ts diff --git a/src/core/stores/ServerMap.ts b/src/old/core/stores/ServerMap.ts similarity index 70% rename from src/core/stores/ServerMap.ts rename to src/old/core/stores/ServerMap.ts index a10a0220..4eb17802 100644 --- a/src/core/stores/ServerMap.ts +++ b/src/old/core/stores/ServerMap.ts @@ -1,7 +1,6 @@ import { computed } from "mobx"; import { Server } from "../domain"; -import { application_store } from "../../application/stores/ApplicationStore"; -import { EnumMap } from "../enums"; +import { EnumMap } from "../../../core/enums"; /** * Map with a guaranteed value per server. @@ -15,6 +14,6 @@ export class ServerMap extends EnumMap { * @returns the value for the current server as set in {@link application_store}. */ @computed get current(): V { - return this.get(application_store.current_server); + return this.get(Server.Ephinea); } } diff --git a/src/core/ui/BigSelect.css b/src/old/core/ui/BigSelect.css similarity index 100% rename from src/core/ui/BigSelect.css rename to src/old/core/ui/BigSelect.css diff --git a/src/core/ui/BigSelect.tsx b/src/old/core/ui/BigSelect.tsx similarity index 100% rename from src/core/ui/BigSelect.tsx rename to src/old/core/ui/BigSelect.tsx diff --git a/src/core/ui/BigTable.css b/src/old/core/ui/BigTable.css similarity index 100% rename from src/core/ui/BigTable.css rename to src/old/core/ui/BigTable.css diff --git a/src/core/ui/BigTable.tsx b/src/old/core/ui/BigTable.tsx similarity index 100% rename from src/core/ui/BigTable.tsx rename to src/old/core/ui/BigTable.tsx diff --git a/src/core/ui/DisabledTextComponent.css b/src/old/core/ui/DisabledTextComponent.css similarity index 100% rename from src/core/ui/DisabledTextComponent.css rename to src/old/core/ui/DisabledTextComponent.css diff --git a/src/core/ui/DisabledTextComponent.tsx b/src/old/core/ui/DisabledTextComponent.tsx similarity index 100% rename from src/core/ui/DisabledTextComponent.tsx rename to src/old/core/ui/DisabledTextComponent.tsx diff --git a/src/core/ui/ErrorBoundary.css b/src/old/core/ui/ErrorBoundary.css similarity index 100% rename from src/core/ui/ErrorBoundary.css rename to src/old/core/ui/ErrorBoundary.css diff --git a/src/core/ui/ErrorBoundary.tsx b/src/old/core/ui/ErrorBoundary.tsx similarity index 100% rename from src/core/ui/ErrorBoundary.tsx rename to src/old/core/ui/ErrorBoundary.tsx diff --git a/src/core/ui/NumberInput.tsx b/src/old/core/ui/NumberInput.tsx similarity index 100% rename from src/core/ui/NumberInput.tsx rename to src/old/core/ui/NumberInput.tsx diff --git a/src/core/ui/RendererComponent.tsx b/src/old/core/ui/RendererComponent.tsx similarity index 95% rename from src/core/ui/RendererComponent.tsx rename to src/old/core/ui/RendererComponent.tsx index b26e897b..2da20538 100644 --- a/src/core/ui/RendererComponent.tsx +++ b/src/old/core/ui/RendererComponent.tsx @@ -1,5 +1,5 @@ import React, { Component, ReactNode } from "react"; -import { Renderer } from "../rendering/Renderer"; +import { Renderer } from "../../../core/rendering/Renderer"; type Props = { renderer: Renderer; diff --git a/src/core/ui/SectionIdIcon.tsx b/src/old/core/ui/SectionIdIcon.tsx similarity index 100% rename from src/core/ui/SectionIdIcon.tsx rename to src/old/core/ui/SectionIdIcon.tsx diff --git a/src/core/ui/TextArea.tsx b/src/old/core/ui/TextArea.tsx similarity index 100% rename from src/core/ui/TextArea.tsx rename to src/old/core/ui/TextArea.tsx diff --git a/src/core/ui/TextInput.tsx b/src/old/core/ui/TextInput.tsx similarity index 100% rename from src/core/ui/TextInput.tsx rename to src/old/core/ui/TextInput.tsx diff --git a/src/core/ui/index.css b/src/old/core/ui/index.css similarity index 100% rename from src/core/ui/index.css rename to src/old/core/ui/index.css diff --git a/src/core/ui/time.ts b/src/old/core/ui/time.ts similarity index 100% rename from src/core/ui/time.ts rename to src/old/core/ui/time.ts diff --git a/src/core/undo.test.ts b/src/old/core/undo.test.ts similarity index 100% rename from src/core/undo.test.ts rename to src/old/core/undo.test.ts diff --git a/src/core/undo.ts b/src/old/core/undo.ts similarity index 100% rename from src/core/undo.ts rename to src/old/core/undo.ts diff --git a/src/dps_calc/stores/DpsCalcStore.ts b/src/old/dps_calc/stores/DpsCalcStore.ts similarity index 100% rename from src/dps_calc/stores/DpsCalcStore.ts rename to src/old/dps_calc/stores/DpsCalcStore.ts diff --git a/src/dps_calc/ui/DpsCalcComponent.tsx b/src/old/dps_calc/ui/DpsCalcComponent.tsx similarity index 100% rename from src/dps_calc/ui/DpsCalcComponent.tsx rename to src/old/dps_calc/ui/DpsCalcComponent.tsx diff --git a/src/hunt_optimizer/domain/index.ts b/src/old/hunt_optimizer/domain/index.ts similarity index 93% rename from src/hunt_optimizer/domain/index.ts rename to src/old/hunt_optimizer/domain/index.ts index 39539f9d..c5c22f9e 100644 --- a/src/hunt_optimizer/domain/index.ts +++ b/src/old/hunt_optimizer/domain/index.ts @@ -1,5 +1,5 @@ -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { computed, observable } from "mobx"; import { ItemType } from "../../core/domain/items"; import { Difficulty, SectionId } from "../../core/domain"; diff --git a/src/hunt_optimizer/persistence/HuntMethodPersister.ts b/src/old/hunt_optimizer/persistence/HuntMethodPersister.ts similarity index 100% rename from src/hunt_optimizer/persistence/HuntMethodPersister.ts rename to src/old/hunt_optimizer/persistence/HuntMethodPersister.ts diff --git a/src/hunt_optimizer/persistence/HuntOptimizerPersister.ts b/src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts similarity index 100% rename from src/hunt_optimizer/persistence/HuntOptimizerPersister.ts rename to src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts diff --git a/src/hunt_optimizer/stores/HuntMethodStore.ts b/src/old/hunt_optimizer/stores/HuntMethodStore.ts similarity index 97% rename from src/hunt_optimizer/stores/HuntMethodStore.ts rename to src/old/hunt_optimizer/stores/HuntMethodStore.ts index 75930ede..be4874b5 100644 --- a/src/hunt_optimizer/stores/HuntMethodStore.ts +++ b/src/old/hunt_optimizer/stores/HuntMethodStore.ts @@ -5,7 +5,7 @@ import { QuestDto } from "../../core/dto"; import { Loadable } from "../../core/Loadable"; import { hunt_method_persister } from "../persistence/HuntMethodPersister"; import { ServerMap } from "../../core/stores/ServerMap"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { HuntMethod, SimpleQuest } from "../domain"; const logger = Logger.get("stores/HuntMethodStore"); diff --git a/src/hunt_optimizer/stores/HuntOptimizerStore.ts b/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts similarity index 95% rename from src/hunt_optimizer/stores/HuntOptimizerStore.ts rename to src/old/hunt_optimizer/stores/HuntOptimizerStore.ts index 3da0ea9e..c22e3b49 100644 --- a/src/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -7,14 +7,14 @@ import { RARE_ENEMY_PROB, SectionId, SectionIds, + Server, } from "../../core/domain"; import { hunt_optimizer_persister } from "../persistence/HuntOptimizerPersister"; -import { application_store } from "../../application/stores/ApplicationStore"; import { hunt_method_store } from "./HuntMethodStore"; import { item_drop_stores } from "./ItemDropStore"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; -import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; +import { npc_data, NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { HuntMethod } from "../domain"; import { ItemType } from "../../core/domain/items"; @@ -334,15 +334,10 @@ class HuntOptimizerStore { } private initialize_persistence = async () => { - this.wanted_items.replace( - await hunt_optimizer_persister.load_wanted_items(application_store.current_server), - ); + this.wanted_items.replace(await hunt_optimizer_persister.load_wanted_items(Server.Ephinea)); autorun(() => { - hunt_optimizer_persister.persist_wanted_items( - application_store.current_server, - this.wanted_items, - ); + hunt_optimizer_persister.persist_wanted_items(Server.Ephinea, this.wanted_items); }); }; } diff --git a/src/hunt_optimizer/stores/ItemDropStore.ts b/src/old/hunt_optimizer/stores/ItemDropStore.ts similarity index 97% rename from src/hunt_optimizer/stores/ItemDropStore.ts rename to src/old/hunt_optimizer/stores/ItemDropStore.ts index 6b79594e..45cae96a 100644 --- a/src/hunt_optimizer/stores/ItemDropStore.ts +++ b/src/old/hunt_optimizer/stores/ItemDropStore.ts @@ -5,7 +5,7 @@ import { Loadable } from "../../core/Loadable"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; import { ServerMap } from "../../core/stores/ServerMap"; import Logger from "js-logger"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { EnemyDrop } from "../domain"; const logger = Logger.get("stores/ItemDropStore"); diff --git a/src/hunt_optimizer/ui/HuntOptimizerComponent.css b/src/old/hunt_optimizer/ui/HuntOptimizerComponent.css similarity index 100% rename from src/hunt_optimizer/ui/HuntOptimizerComponent.css rename to src/old/hunt_optimizer/ui/HuntOptimizerComponent.css diff --git a/src/hunt_optimizer/ui/HuntOptimizerComponent.tsx b/src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx similarity index 100% rename from src/hunt_optimizer/ui/HuntOptimizerComponent.tsx rename to src/old/hunt_optimizer/ui/HuntOptimizerComponent.tsx diff --git a/src/hunt_optimizer/ui/MethodsComponent.css b/src/old/hunt_optimizer/ui/MethodsComponent.css similarity index 100% rename from src/hunt_optimizer/ui/MethodsComponent.css rename to src/old/hunt_optimizer/ui/MethodsComponent.css diff --git a/src/hunt_optimizer/ui/MethodsComponent.tsx b/src/old/hunt_optimizer/ui/MethodsComponent.tsx similarity index 97% rename from src/hunt_optimizer/ui/MethodsComponent.tsx rename to src/old/hunt_optimizer/ui/MethodsComponent.tsx index 2c8efe0c..063dba4e 100644 --- a/src/hunt_optimizer/ui/MethodsComponent.tsx +++ b/src/old/hunt_optimizer/ui/MethodsComponent.tsx @@ -6,12 +6,12 @@ import { AutoSizer, Index, SortDirection } from "react-virtualized"; import { hunt_method_store } from "../stores/HuntMethodStore"; import { BigTable, Column, ColumnSort } from "../../core/ui/BigTable"; import styles from "./MethodsComponent.css"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { ENEMY_NPC_TYPES, npc_data, NpcType, -} from "../../core/data_formats/parsing/quest/npc_types"; +} from "../../../core/data_formats/parsing/quest/npc_types"; import { HuntMethod } from "../domain"; @observer diff --git a/src/hunt_optimizer/ui/OptimizationResultComponent.css b/src/old/hunt_optimizer/ui/OptimizationResultComponent.css similarity index 100% rename from src/hunt_optimizer/ui/OptimizationResultComponent.css rename to src/old/hunt_optimizer/ui/OptimizationResultComponent.css diff --git a/src/hunt_optimizer/ui/OptimizationResultComponent.tsx b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx similarity index 98% rename from src/hunt_optimizer/ui/OptimizationResultComponent.tsx rename to src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx index e4977804..217d1ea1 100644 --- a/src/hunt_optimizer/ui/OptimizationResultComponent.tsx +++ b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx @@ -8,7 +8,7 @@ import { BigTable, Column } from "../../core/ui/BigTable"; import { SectionIdIcon } from "../../core/ui/SectionIdIcon"; import { hours_to_string } from "../../core/ui/time"; import styles from "./OptimizationResultComponent.css"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; @observer export class OptimizationResultComponent extends Component { diff --git a/src/hunt_optimizer/ui/OptimizerComponent.css b/src/old/hunt_optimizer/ui/OptimizerComponent.css similarity index 100% rename from src/hunt_optimizer/ui/OptimizerComponent.css rename to src/old/hunt_optimizer/ui/OptimizerComponent.css diff --git a/src/hunt_optimizer/ui/OptimizerComponent.tsx b/src/old/hunt_optimizer/ui/OptimizerComponent.tsx similarity index 100% rename from src/hunt_optimizer/ui/OptimizerComponent.tsx rename to src/old/hunt_optimizer/ui/OptimizerComponent.tsx diff --git a/src/hunt_optimizer/ui/WantedItemsComponent.css b/src/old/hunt_optimizer/ui/WantedItemsComponent.css similarity index 100% rename from src/hunt_optimizer/ui/WantedItemsComponent.css rename to src/old/hunt_optimizer/ui/WantedItemsComponent.css diff --git a/src/hunt_optimizer/ui/WantedItemsComponent.tsx b/src/old/hunt_optimizer/ui/WantedItemsComponent.tsx similarity index 100% rename from src/hunt_optimizer/ui/WantedItemsComponent.tsx rename to src/old/hunt_optimizer/ui/WantedItemsComponent.tsx diff --git a/src/quest_editor/domain/ObservableArea.ts b/src/old/quest_editor/domain/ObservableArea.ts similarity index 100% rename from src/quest_editor/domain/ObservableArea.ts rename to src/old/quest_editor/domain/ObservableArea.ts diff --git a/src/quest_editor/domain/ObservableAreaVariant.ts b/src/old/quest_editor/domain/ObservableAreaVariant.ts similarity index 100% rename from src/quest_editor/domain/ObservableAreaVariant.ts rename to src/old/quest_editor/domain/ObservableAreaVariant.ts diff --git a/src/quest_editor/domain/ObservableQuest.ts b/src/old/quest_editor/domain/ObservableQuest.ts similarity index 97% rename from src/quest_editor/domain/ObservableQuest.ts rename to src/old/quest_editor/domain/ObservableQuest.ts index d830888c..6b896c73 100644 --- a/src/quest_editor/domain/ObservableQuest.ts +++ b/src/old/quest_editor/domain/ObservableQuest.ts @@ -1,8 +1,8 @@ import { action, computed, observable } from "mobx"; -import { check_episode, Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { check_episode, Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { ObservableAreaVariant } from "./ObservableAreaVariant"; import { area_store } from "../stores/AreaStore"; -import { DatUnknown } from "../../core/data_formats/parsing/quest/dat"; +import { DatUnknown } from "../../../core/data_formats/parsing/quest/dat"; import { Segment } from "../scripting/instructions"; import Logger from "js-logger"; import { ObservableQuestNpc, ObservableQuestObject } from "./observable_quest_entities"; diff --git a/src/quest_editor/domain/Section.ts b/src/old/quest_editor/domain/Section.ts similarity index 93% rename from src/quest_editor/domain/Section.ts rename to src/old/quest_editor/domain/Section.ts index d6ef5308..c5f38592 100644 --- a/src/quest_editor/domain/Section.ts +++ b/src/old/quest_editor/domain/Section.ts @@ -1,4 +1,4 @@ -import { Vec3 } from "../../core/data_formats/vector"; +import { Vec3 } from "../../../core/data_formats/vector"; export class Section { readonly id: number; diff --git a/src/quest_editor/domain/observable_quest_entities.ts b/src/old/quest_editor/domain/observable_quest_entities.ts similarity index 94% rename from src/quest_editor/domain/observable_quest_entities.ts rename to src/old/quest_editor/domain/observable_quest_entities.ts index 7cc1f488..c2cd789b 100644 --- a/src/quest_editor/domain/observable_quest_entities.ts +++ b/src/old/quest_editor/domain/observable_quest_entities.ts @@ -1,9 +1,9 @@ -import { ObjectType } from "../../core/data_formats/parsing/quest/object_types"; +import { ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; import { action, computed, observable } from "mobx"; -import { Vec3 } from "../../core/data_formats/vector"; -import { EntityType } from "../../core/data_formats/parsing/quest/entities"; +import { Vec3 } from "../../../core/data_formats/vector"; +import { EntityType } from "../../../core/data_formats/parsing/quest/entities"; import { Section } from "./Section"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; /** * Abstract class from which ObservableQuestNpc and ObservableQuestObject derive. diff --git a/src/quest_editor/loading/LoadingCache.ts b/src/old/quest_editor/loading/LoadingCache.ts similarity index 100% rename from src/quest_editor/loading/LoadingCache.ts rename to src/old/quest_editor/loading/LoadingCache.ts diff --git a/src/quest_editor/loading/areas.ts b/src/old/quest_editor/loading/areas.ts similarity index 91% rename from src/quest_editor/loading/areas.ts rename to src/old/quest_editor/loading/areas.ts index 34a20559..df387b28 100644 --- a/src/quest_editor/loading/areas.ts +++ b/src/old/quest_editor/loading/areas.ts @@ -1,16 +1,16 @@ import { Object3D } from "three"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { parse_area_collision_geometry } from "../../core/data_formats/parsing/area_collision_geometry"; -import { parse_area_geometry } from "../../core/data_formats/parsing/area_geometry"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { parse_area_collision_geometry } from "../../../core/data_formats/parsing/area_collision_geometry"; +import { parse_area_geometry } from "../../../core/data_formats/parsing/area_geometry"; import { area_collision_geometry_to_object_3d, area_geometry_to_sections_and_object_3d, } from "../rendering/conversion/areas"; -import { load_array_buffer } from "../../core/loading"; +import { load_array_buffer } from "../../../core/loading"; import { LoadingCache } from "./LoadingCache"; import { Section } from "../domain/Section"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; const render_geometry_cache = new LoadingCache< string, diff --git a/src/quest_editor/loading/entities.ts b/src/old/quest_editor/loading/entities.ts similarity index 92% rename from src/quest_editor/loading/entities.ts rename to src/old/quest_editor/loading/entities.ts index 780246ee..8003fea5 100644 --- a/src/quest_editor/loading/entities.ts +++ b/src/old/quest_editor/loading/entities.ts @@ -1,15 +1,15 @@ import { Texture, CylinderBufferGeometry, BufferGeometry } from "three"; import Logger from "js-logger"; import { LoadingCache } from "./LoadingCache"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry"; -import { parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja"; -import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures"; -import { load_array_buffer } from "../../core/loading"; -import { object_data, ObjectType } from "../../core/data_formats/parsing/quest/object_types"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { ninja_object_to_buffer_geometry } from "../../../core/rendering/conversion/ninja_geometry"; +import { parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja"; +import { parse_xvm } from "../../../core/data_formats/parsing/ninja/texture"; +import { xvm_to_textures } from "../../../core/rendering/conversion/ninja_textures"; +import { load_array_buffer } from "../../../core/loading"; +import { object_data, ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; const logger = Logger.get("loading/entities"); diff --git a/src/quest_editor/persistence/QuestEditorUiPersister.ts b/src/old/quest_editor/persistence/QuestEditorUiPersister.ts similarity index 100% rename from src/quest_editor/persistence/QuestEditorUiPersister.ts rename to src/old/quest_editor/persistence/QuestEditorUiPersister.ts diff --git a/src/quest_editor/rendering/QuestEntityControls.ts b/src/old/quest_editor/rendering/QuestEntityControls.ts similarity index 99% rename from src/quest_editor/rendering/QuestEntityControls.ts rename to src/old/quest_editor/rendering/QuestEntityControls.ts index 4a2bb181..78adb26e 100644 --- a/src/quest_editor/rendering/QuestEntityControls.ts +++ b/src/old/quest_editor/rendering/QuestEntityControls.ts @@ -1,6 +1,6 @@ import { autorun } from "mobx"; import { Intersection, Mesh, MeshLambertMaterial, Plane, Raycaster, Vector2, Vector3 } from "three"; -import { Vec3 } from "../../core/data_formats/vector"; +import { Vec3 } from "../../../core/data_formats/vector"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { AreaUserData } from "./conversion/areas"; import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities"; diff --git a/src/quest_editor/rendering/QuestModelManager.ts b/src/old/quest_editor/rendering/QuestModelManager.ts similarity index 100% rename from src/quest_editor/rendering/QuestModelManager.ts rename to src/old/quest_editor/rendering/QuestModelManager.ts diff --git a/src/quest_editor/rendering/QuestRenderer.ts b/src/old/quest_editor/rendering/QuestRenderer.ts similarity index 98% rename from src/quest_editor/rendering/QuestRenderer.ts rename to src/old/quest_editor/rendering/QuestRenderer.ts index 31bb1b34..9ba48d62 100644 --- a/src/quest_editor/rendering/QuestRenderer.ts +++ b/src/old/quest_editor/rendering/QuestRenderer.ts @@ -3,7 +3,7 @@ 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 { Renderer } from "../../../core/rendering/Renderer"; import { EntityUserData } from "./conversion/entities"; import { ObservableQuestEntity } from "../domain/observable_quest_entities"; diff --git a/src/quest_editor/rendering/conversion/areas.ts b/src/old/quest_editor/rendering/conversion/areas.ts similarity index 91% rename from src/quest_editor/rendering/conversion/areas.ts rename to src/old/quest_editor/rendering/conversion/areas.ts index 4588d192..9456d2fc 100644 --- a/src/quest_editor/rendering/conversion/areas.ts +++ b/src/old/quest_editor/rendering/conversion/areas.ts @@ -10,10 +10,10 @@ import { Vector3, Color, } from "three"; -import { CollisionObject } from "../../../core/data_formats/parsing/area_collision_geometry"; -import { RenderObject } from "../../../core/data_formats/parsing/area_geometry"; -import { GeometryBuilder } from "../../../core/rendering/conversion/GeometryBuilder"; -import { ninja_object_to_geometry_builder } from "../../../core/rendering/conversion/ninja_geometry"; +import { CollisionObject } from "../../../../core/data_formats/parsing/area_collision_geometry"; +import { RenderObject } from "../../../../core/data_formats/parsing/area_geometry"; +import { GeometryBuilder } from "../../../../core/rendering/conversion/GeometryBuilder"; +import { ninja_object_to_geometry_builder } from "../../../../core/rendering/conversion/ninja_geometry"; import { Section } from "../../domain/Section"; const materials = [ diff --git a/src/quest_editor/rendering/conversion/entities.ts b/src/old/quest_editor/rendering/conversion/entities.ts similarity index 89% rename from src/quest_editor/rendering/conversion/entities.ts rename to src/old/quest_editor/rendering/conversion/entities.ts index eddf4256..94bc4c56 100644 --- a/src/quest_editor/rendering/conversion/entities.ts +++ b/src/old/quest_editor/rendering/conversion/entities.ts @@ -1,7 +1,7 @@ import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial, Texture } from "three"; -import { create_mesh } from "../../../core/rendering/conversion/create_mesh"; -import { ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; -import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; +import { create_mesh } from "../../../../core/rendering/conversion/create_mesh"; +import { ObjectType } from "../../../../core/data_formats/parsing/quest/object_types"; +import { NpcType } from "../../../../core/data_formats/parsing/quest/npc_types"; import { ObservableQuestEntity, ObservableQuestNpc, diff --git a/src/quest_editor/scripting/AssemblyAnalyser.ts b/src/old/quest_editor/scripting/AssemblyAnalyser.ts similarity index 100% rename from src/quest_editor/scripting/AssemblyAnalyser.ts rename to src/old/quest_editor/scripting/AssemblyAnalyser.ts diff --git a/src/quest_editor/scripting/AssemblyLexer.test.ts b/src/old/quest_editor/scripting/AssemblyLexer.test.ts similarity index 100% rename from src/quest_editor/scripting/AssemblyLexer.test.ts rename to src/old/quest_editor/scripting/AssemblyLexer.test.ts diff --git a/src/quest_editor/scripting/AssemblyLexer.ts b/src/old/quest_editor/scripting/AssemblyLexer.ts similarity index 100% rename from src/quest_editor/scripting/AssemblyLexer.ts rename to src/old/quest_editor/scripting/AssemblyLexer.ts diff --git a/src/quest_editor/scripting/assembly.test.ts b/src/old/quest_editor/scripting/assembly.test.ts similarity index 100% rename from src/quest_editor/scripting/assembly.test.ts rename to src/old/quest_editor/scripting/assembly.test.ts diff --git a/src/quest_editor/scripting/assembly.ts b/src/old/quest_editor/scripting/assembly.ts similarity index 100% rename from src/quest_editor/scripting/assembly.ts rename to src/old/quest_editor/scripting/assembly.ts diff --git a/src/quest_editor/scripting/assembly_worker.ts b/src/old/quest_editor/scripting/assembly_worker.ts similarity index 100% rename from src/quest_editor/scripting/assembly_worker.ts rename to src/old/quest_editor/scripting/assembly_worker.ts diff --git a/src/quest_editor/scripting/assembly_worker_messages.ts b/src/old/quest_editor/scripting/assembly_worker_messages.ts similarity index 100% rename from src/quest_editor/scripting/assembly_worker_messages.ts rename to src/old/quest_editor/scripting/assembly_worker_messages.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts b/src/old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts rename to src/old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts b/src/old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts rename to src/old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts b/src/old/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts rename to src/old/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/ValueSet.ts b/src/old/quest_editor/scripting/data_flow_analysis/ValueSet.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/ValueSet.ts rename to src/old/quest_editor/scripting/data_flow_analysis/ValueSet.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/register_value.test.ts b/src/old/quest_editor/scripting/data_flow_analysis/register_value.test.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/register_value.test.ts rename to src/old/quest_editor/scripting/data_flow_analysis/register_value.test.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/register_value.ts b/src/old/quest_editor/scripting/data_flow_analysis/register_value.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/register_value.ts rename to src/old/quest_editor/scripting/data_flow_analysis/register_value.ts diff --git a/src/quest_editor/scripting/data_flow_analysis/stack_value.ts b/src/old/quest_editor/scripting/data_flow_analysis/stack_value.ts similarity index 100% rename from src/quest_editor/scripting/data_flow_analysis/stack_value.ts rename to src/old/quest_editor/scripting/data_flow_analysis/stack_value.ts diff --git a/src/quest_editor/scripting/disassembly.test.ts b/src/old/quest_editor/scripting/disassembly.test.ts similarity index 83% rename from src/quest_editor/scripting/disassembly.test.ts rename to src/old/quest_editor/scripting/disassembly.test.ts index b13a32bc..9c3fa6b3 100644 --- a/src/quest_editor/scripting/disassembly.test.ts +++ b/src/old/quest_editor/scripting/disassembly.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from "fs"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { prs_decompress } from "../../core/data_formats/compression/prs/decompress"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { BufferCursor } from "../../core/data_formats/cursor/BufferCursor"; -import { parse_bin, write_bin } from "../../core/data_formats/parsing/quest/bin"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { prs_decompress } from "../../../core/data_formats/compression/prs/decompress"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { BufferCursor } from "../../../core/data_formats/cursor/BufferCursor"; +import { parse_bin, write_bin } from "../../../core/data_formats/parsing/quest/bin"; import { assemble } from "./assembly"; import { disassemble } from "./disassembly"; diff --git a/src/quest_editor/scripting/disassembly.ts b/src/old/quest_editor/scripting/disassembly.ts similarity index 100% rename from src/quest_editor/scripting/disassembly.ts rename to src/old/quest_editor/scripting/disassembly.ts diff --git a/src/quest_editor/scripting/instructions.ts b/src/old/quest_editor/scripting/instructions.ts similarity index 100% rename from src/quest_editor/scripting/instructions.ts rename to src/old/quest_editor/scripting/instructions.ts diff --git a/src/quest_editor/scripting/opcodes.ts b/src/old/quest_editor/scripting/opcodes.ts similarity index 100% rename from src/quest_editor/scripting/opcodes.ts rename to src/old/quest_editor/scripting/opcodes.ts diff --git a/src/quest_editor/scripting/vm/index.ts b/src/old/quest_editor/scripting/vm/index.ts similarity index 100% rename from src/quest_editor/scripting/vm/index.ts rename to src/old/quest_editor/scripting/vm/index.ts diff --git a/src/quest_editor/stores/AreaStore.ts b/src/old/quest_editor/stores/AreaStore.ts similarity index 91% rename from src/quest_editor/stores/AreaStore.ts rename to src/old/quest_editor/stores/AreaStore.ts index fb6e53f7..6f34f489 100644 --- a/src/quest_editor/stores/AreaStore.ts +++ b/src/old/quest_editor/stores/AreaStore.ts @@ -1,6 +1,6 @@ import { load_area_sections } from "../loading/areas"; -import { Episode, EPISODES } from "../../core/data_formats/parsing/quest/Episode"; -import { get_areas_for_episode } from "../../core/data_formats/parsing/quest/areas"; +import { Episode, EPISODES } from "../../../core/data_formats/parsing/quest/Episode"; +import { get_areas_for_episode } from "../../../core/data_formats/parsing/quest/areas"; import { ObservableAreaVariant } from "../domain/ObservableAreaVariant"; import { ObservableArea } from "../domain/ObservableArea"; import { Section } from "../domain/Section"; diff --git a/src/quest_editor/stores/QuestEditorStore.ts b/src/old/quest_editor/stores/QuestEditorStore.ts similarity index 90% rename from src/quest_editor/stores/QuestEditorStore.ts rename to src/old/quest_editor/stores/QuestEditorStore.ts index 472d1a58..a70febce 100644 --- a/src/quest_editor/stores/QuestEditorStore.ts +++ b/src/old/quest_editor/stores/QuestEditorStore.ts @@ -1,16 +1,15 @@ import Logger from "js-logger"; import { action, flow, observable } from "mobx"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/quest"; -import { Vec3 } from "../../core/data_formats/vector"; -import { read_file } from "../../core/read_file"; -import { SimpleUndo, undo_manager, UndoStack } from "../../core/undo"; -import { application_store } from "../../application/stores/ApplicationStore"; +import { Endianness } from "../../../core/data_formats/Endianness"; +import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; +import { parse_quest, write_quest_qst } from "../../../core/data_formats/parsing/quest"; +import { Vec3 } from "../../../core/data_formats/vector"; +import { read_file } from "../../../core/read_file"; +import { SimpleUndo, UndoStack } from "../../core/undo"; import { area_store } from "./AreaStore"; import { create_new_quest } from "./quest_creation"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; -import { entity_data } from "../../core/data_formats/parsing/quest/entities"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; +import { entity_data } from "../../../core/data_formats/parsing/quest/entities"; import { ObservableQuest } from "../domain/ObservableQuest"; import { ObservableArea } from "../domain/ObservableArea"; import { Section } from "../domain/Section"; @@ -38,19 +37,19 @@ class QuestEditorStore { @observable save_dialog_open: boolean = false; constructor() { - application_store.on_global_keyup("quest_editor", "Ctrl-Z", () => { - // Let Monaco handle its own key bindings. - if (undo_manager.current !== this.script_undo) { - undo_manager.undo(); - } - }); - application_store.on_global_keyup("quest_editor", "Ctrl-Shift-Z", () => { - // Let Monaco handle its own key bindings. - if (undo_manager.current !== this.script_undo) { - undo_manager.redo(); - } - }); - application_store.on_global_keyup("quest_editor", "Ctrl-Alt-D", this.toggle_debug); + // application_store.on_global_keyup("quest_editor", "Ctrl-Z", () => { + // // Let Monaco handle its own key bindings. + // if (undo_manager.current !== this.script_undo) { + // undo_manager.undo(); + // } + // }); + // application_store.on_global_keyup("quest_editor", "Ctrl-Shift-Z", () => { + // // Let Monaco handle its own key bindings. + // if (undo_manager.current !== this.script_undo) { + // undo_manager.redo(); + // } + // }); + // application_store.on_global_keyup("quest_editor", "Ctrl-Alt-D", this.toggle_debug); } @action diff --git a/src/quest_editor/stores/quest_creation.ts b/src/old/quest_editor/stores/quest_creation.ts similarity index 98% rename from src/quest_editor/stores/quest_creation.ts rename to src/old/quest_editor/stores/quest_creation.ts index 205e9802..a691468a 100644 --- a/src/quest_editor/stores/quest_creation.ts +++ b/src/old/quest_editor/stores/quest_creation.ts @@ -1,9 +1,9 @@ -import { Vec3 } from "../../core/data_formats/vector"; +import { Vec3 } from "../../../core/data_formats/vector"; import { Instruction, SegmentType } from "../scripting/instructions"; import { Opcode } from "../scripting/opcodes"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; -import { ObjectType } from "../../core/data_formats/parsing/quest/object_types"; -import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; +import { ObjectType } from "../../../core/data_formats/parsing/quest/object_types"; +import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { ObservableQuest } from "../domain/ObservableQuest"; import { ObservableQuestNpc, ObservableQuestObject } from "../domain/observable_quest_entities"; diff --git a/src/quest_editor/ui/AddObjectComponent.tsx b/src/old/quest_editor/ui/AddObjectComponent.tsx similarity index 94% rename from src/quest_editor/ui/AddObjectComponent.tsx rename to src/old/quest_editor/ui/AddObjectComponent.tsx index dab83b54..416077a0 100644 --- a/src/quest_editor/ui/AddObjectComponent.tsx +++ b/src/old/quest_editor/ui/AddObjectComponent.tsx @@ -5,7 +5,7 @@ import { object_data, OBJECT_TYPES, ObjectType, -} from "../../core/data_formats/parsing/quest/object_types"; +} from "../../../core/data_formats/parsing/quest/object_types"; const drag_helper = document.createElement("div"); drag_helper.id = "drag_helper"; diff --git a/src/quest_editor/ui/AssemblyEditorComponent.css b/src/old/quest_editor/ui/AssemblyEditorComponent.css similarity index 100% rename from src/quest_editor/ui/AssemblyEditorComponent.css rename to src/old/quest_editor/ui/AssemblyEditorComponent.css diff --git a/src/quest_editor/ui/AssemblyEditorComponent.tsx b/src/old/quest_editor/ui/AssemblyEditorComponent.tsx similarity index 100% rename from src/quest_editor/ui/AssemblyEditorComponent.tsx rename to src/old/quest_editor/ui/AssemblyEditorComponent.tsx diff --git a/src/quest_editor/ui/EntityInfoComponent.css b/src/old/quest_editor/ui/EntityInfoComponent.css similarity index 100% rename from src/quest_editor/ui/EntityInfoComponent.css rename to src/old/quest_editor/ui/EntityInfoComponent.css diff --git a/src/quest_editor/ui/EntityInfoComponent.tsx b/src/old/quest_editor/ui/EntityInfoComponent.tsx similarity index 96% rename from src/quest_editor/ui/EntityInfoComponent.tsx rename to src/old/quest_editor/ui/EntityInfoComponent.tsx index 3e4a5acb..9733b4eb 100644 --- a/src/quest_editor/ui/EntityInfoComponent.tsx +++ b/src/old/quest_editor/ui/EntityInfoComponent.tsx @@ -2,11 +2,11 @@ import { InputNumber } from "antd"; import { autorun, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import React, { Component, PureComponent, ReactNode } from "react"; -import { Vec3 } from "../../core/data_formats/vector"; +import { Vec3 } from "../../../core/data_formats/vector"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent"; import styles from "./EntityInfoComponent.css"; -import { entity_data, entity_type_to_string } from "../../core/data_formats/parsing/quest/entities"; +import { entity_data, entity_type_to_string } from "../../../core/data_formats/parsing/quest/entities"; import { ObservableQuestEntity, ObservableQuestNpc } from "../domain/observable_quest_entities"; @observer diff --git a/src/quest_editor/ui/NpcCountsComponent.css b/src/old/quest_editor/ui/NpcCountsComponent.css similarity index 100% rename from src/quest_editor/ui/NpcCountsComponent.css rename to src/old/quest_editor/ui/NpcCountsComponent.css diff --git a/src/quest_editor/ui/NpcCountsComponent.tsx b/src/old/quest_editor/ui/NpcCountsComponent.tsx similarity index 93% rename from src/quest_editor/ui/NpcCountsComponent.tsx rename to src/old/quest_editor/ui/NpcCountsComponent.tsx index 5eb1b6a6..7473eb35 100644 --- a/src/quest_editor/ui/NpcCountsComponent.tsx +++ b/src/old/quest_editor/ui/NpcCountsComponent.tsx @@ -1,6 +1,6 @@ import React, { Component, ReactNode } from "react"; import styles from "./NpcCountsComponent.css"; -import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; +import { npc_data, NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { observer } from "mobx-react"; diff --git a/src/quest_editor/ui/QuestEditorComponent.css b/src/old/quest_editor/ui/QuestEditorComponent.css similarity index 100% rename from src/quest_editor/ui/QuestEditorComponent.css rename to src/old/quest_editor/ui/QuestEditorComponent.css diff --git a/src/quest_editor/ui/QuestEditorComponent.tsx b/src/old/quest_editor/ui/QuestEditorComponent.tsx similarity index 100% rename from src/quest_editor/ui/QuestEditorComponent.tsx rename to src/old/quest_editor/ui/QuestEditorComponent.tsx diff --git a/src/quest_editor/ui/QuestInfoComponent.css b/src/old/quest_editor/ui/QuestInfoComponent.css similarity index 100% rename from src/quest_editor/ui/QuestInfoComponent.css rename to src/old/quest_editor/ui/QuestInfoComponent.css diff --git a/src/quest_editor/ui/QuestInfoComponent.tsx b/src/old/quest_editor/ui/QuestInfoComponent.tsx similarity index 98% rename from src/quest_editor/ui/QuestInfoComponent.tsx rename to src/old/quest_editor/ui/QuestInfoComponent.tsx index 33c605e1..7fe3739b 100644 --- a/src/quest_editor/ui/QuestInfoComponent.tsx +++ b/src/old/quest_editor/ui/QuestInfoComponent.tsx @@ -3,7 +3,7 @@ import React, { ChangeEvent, Component, ReactNode } from "react"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent"; import styles from "./QuestInfoComponent.css"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { NumberInput } from "../../core/ui/NumberInput"; import { TextInput } from "../../core/ui/TextInput"; import { TextArea } from "../../core/ui/TextArea"; diff --git a/src/quest_editor/ui/QuestRendererComponent.tsx b/src/old/quest_editor/ui/QuestRendererComponent.tsx similarity index 100% rename from src/quest_editor/ui/QuestRendererComponent.tsx rename to src/old/quest_editor/ui/QuestRendererComponent.tsx diff --git a/src/quest_editor/ui/Toolbar.css b/src/old/quest_editor/ui/Toolbar.css similarity index 100% rename from src/quest_editor/ui/Toolbar.css rename to src/old/quest_editor/ui/Toolbar.css diff --git a/src/quest_editor/ui/Toolbar.tsx b/src/old/quest_editor/ui/Toolbar.tsx similarity index 98% rename from src/quest_editor/ui/Toolbar.tsx rename to src/old/quest_editor/ui/Toolbar.tsx index cbc16713..2e5b73b6 100644 --- a/src/quest_editor/ui/Toolbar.tsx +++ b/src/old/quest_editor/ui/Toolbar.tsx @@ -7,7 +7,7 @@ import { area_store } from "../stores/AreaStore"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { undo_manager } from "../../core/undo"; import styles from "./Toolbar.css"; -import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; @observer export class Toolbar extends Component { diff --git a/src/new/viewer/domain/CharacterClassAnimation.ts b/src/viewer/domain/CharacterClassAnimation.ts similarity index 100% rename from src/new/viewer/domain/CharacterClassAnimation.ts rename to src/viewer/domain/CharacterClassAnimation.ts diff --git a/src/new/viewer/domain/CharacterClassModel.ts b/src/viewer/domain/CharacterClassModel.ts similarity index 100% rename from src/new/viewer/domain/CharacterClassModel.ts rename to src/viewer/domain/CharacterClassModel.ts diff --git a/src/viewer/domain/index.ts b/src/viewer/domain/index.ts deleted file mode 100644 index 5fbf724a..00000000 --- a/src/viewer/domain/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class PlayerModel { - constructor( - readonly name: string, - readonly head_style_count: number, - readonly hair_styles_count: number, - readonly hair_styles_with_accessory: Set, - ) {} -} - -export class PlayerAnimation { - constructor(readonly id: number, readonly name: string) {} -} diff --git a/src/new/viewer/gui/ModelView.css b/src/viewer/gui/ModelView.css similarity index 100% rename from src/new/viewer/gui/ModelView.css rename to src/viewer/gui/ModelView.css diff --git a/src/new/viewer/gui/ModelView.ts b/src/viewer/gui/ModelView.ts similarity index 93% rename from src/new/viewer/gui/ModelView.ts rename to src/viewer/gui/ModelView.ts index cfc5a054..d5832be8 100644 --- a/src/new/viewer/gui/ModelView.ts +++ b/src/viewer/gui/ModelView.ts @@ -10,8 +10,9 @@ import { View } from "../../core/gui/View"; import { FileButton } from "../../core/gui/FileButton"; import { CheckBox } from "../../core/gui/CheckBox"; import { NumberInput } from "../../core/gui/NumberInput"; -import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; import { Label } from "../../core/gui/Label"; +import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 150; @@ -43,6 +44,18 @@ export class ModelView extends ResizableView { this.element.append(this.tool_bar_view.element, this.container_element); model_store.current_model.set(model_store.models[5]); + + this.renderer_view.start_rendering(); + + this.disposable( + gui_store.tool.observe(tool => { + if (tool === GuiTool.Viewer) { + this.renderer_view.start_rendering(); + } else { + this.renderer_view.stop_rendering(); + } + }), + ); } resize(width: number, height: number): this { diff --git a/src/new/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts similarity index 100% rename from src/new/viewer/gui/TextureView.ts rename to src/viewer/gui/TextureView.ts diff --git a/src/new/viewer/gui/ViewerView.ts b/src/viewer/gui/ViewerView.ts similarity index 100% rename from src/new/viewer/gui/ViewerView.ts rename to src/viewer/gui/ViewerView.ts diff --git a/src/viewer/loading/character_class.ts b/src/viewer/loading/character_class.ts index a0d10f74..31c2dd02 100644 --- a/src/viewer/loading/character_class.ts +++ b/src/viewer/loading/character_class.ts @@ -8,12 +8,14 @@ export async function get_character_class_data( return await load_array_buffer(character_class_to_url(player_class, body_part, no)); } -export async function get_character_class_animation_data(animation_id: number): Promise { +export async function get_character_class_animation_data( + animation_id: number, +): Promise { return await load_array_buffer( `/player/animation/animation_${animation_id.toString().padStart(3, "0")}.njm`, ); } function character_class_to_url(player_class: string, body_part: string, no?: number): string { - return `/player/${player_class}${body_part}${no == undefined ? "" : no}.nj`; + return `/player/${player_class}${body_part}${no == null ? "" : no}.nj`; } diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index d19f4404..100ba702 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -1,45 +1,55 @@ -import { autorun } from "mobx"; -import { Object3D, PerspectiveCamera, SkeletonHelper, Vector3 } from "three"; -import { model_viewer_store } from "../stores/ModelViewerStore"; +import { + AnimationAction, + AnimationClip, + AnimationMixer, + Clock, + DoubleSide, + Mesh, + MeshLambertMaterial, + Object3D, + PerspectiveCamera, + SkeletonHelper, + SkinnedMesh, + Vector3, +} from "three"; +import { model_store } from "../stores/ModelStore"; +import { Disposable } from "../../core/observable/Disposable"; +import { NjMotion } from "../../core/data_formats/parsing/ninja/motion"; +import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures"; +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 { + create_animation_clip, + PSO_FRAME_RATE, +} from "../../core/rendering/conversion/ninja_animation"; import { Renderer } from "../../core/rendering/Renderer"; -let renderer: ModelRenderer | undefined; - -export function get_model_renderer(): ModelRenderer { - if (!renderer) renderer = new ModelRenderer(); - return renderer; -} - -export class ModelRenderer extends Renderer { - private model?: Object3D; +export class ModelRenderer extends Renderer implements Disposable { + private readonly perspective_camera: PerspectiveCamera; + private readonly disposables: Disposable[] = []; + private readonly clock = new Clock(); + private mesh?: Object3D; private skeleton_helper?: SkeletonHelper; - private perspective_camera: PerspectiveCamera; + private animation?: { + mixer: AnimationMixer; + clip: AnimationClip; + action: AnimationAction; + }; constructor() { super(new PerspectiveCamera(75, 1, 1, 200)); this.perspective_camera = this.camera as PerspectiveCamera; - autorun(() => { - this.set_model(model_viewer_store.current_obj3d); - - const show_skeleton = model_viewer_store.show_skeleton; - - if (this.skeleton_helper) { - this.skeleton_helper.visible = show_skeleton; - this.schedule_render(); - } - - 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(); - } - }); + this.disposables.push( + model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), + model_store.current_xvm.observe(this.nj_data_or_xvm_changed), + model_store.current_nj_motion.observe(this.nj_motion_changed), + model_store.show_skeleton.observe(this.show_skeleton_changed), + model_store.animation_playing.observe(this.animation_playing_changed), + model_store.animation_frame_rate.observe(this.animation_frame_rate_changed), + model_store.animation_frame.observe(this.animation_frame_changed), + ); } set_size(width: number, height: number): void { @@ -48,39 +58,157 @@ export class ModelRenderer extends Renderer { 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(); + if (this.animation) { + this.animation.mixer.update(this.clock.getDelta()); } this.light_holder.quaternion.copy(this.perspective_camera.quaternion); super.render(); - if (model_viewer_store.animation && !model_viewer_store.animation.action.paused) { + if (this.animation && !this.animation.action.paused) { + this.update_animation_frame(); this.schedule_render(); } } - private set_model(model?: Object3D): void { - if (this.model !== model) { - if (this.model) { - this.scene.remove(this.model); - this.scene.remove(this.skeleton_helper!); - this.skeleton_helper = undefined; + private nj_data_or_xvm_changed = () => { + if (this.mesh) { + this.scene.remove(this.mesh); + this.mesh = undefined; + this.scene.remove(this.skeleton_helper!); + this.skeleton_helper = undefined; + } + + if (this.animation) { + this.animation.mixer.stopAllAction(); + if (this.mesh) this.animation.mixer.uncacheRoot(this.mesh); + this.animation = undefined; + } + + const nj_data = model_store.current_nj_data.get(); + + if (nj_data) { + const { nj_object, has_skeleton } = nj_data; + + let mesh: Mesh; + + const xvm = model_store.current_xvm.get(); + const textures = xvm ? xvm_to_textures(xvm) : undefined; + + 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); } - 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)); - } + // 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.model = model; + this.mesh = mesh; + this.scene.add(mesh); + + this.skeleton_helper = new SkeletonHelper(mesh); + 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.schedule_render(); + }; + + private nj_motion_changed = (nj_motion?: NjMotion) => { + let mixer!: AnimationMixer; + + if (this.animation) { + this.animation.mixer.stopAllAction(); + mixer = this.animation.mixer; + } + + const nj_data = model_store.current_nj_data.get(); + + if (!this.mesh || !(this.mesh instanceof SkinnedMesh) || !nj_motion || !nj_data) return; + + if (!this.animation) { + mixer = new AnimationMixer(this.mesh); + } + + const clip = create_animation_clip(nj_data.nj_object, nj_motion); + + this.animation = { + mixer, + clip, + action: mixer.clipAction(clip), + }; + + this.clock.start(); + this.animation.action.play(); + this.schedule_render(); + }; + + private show_skeleton_changed = (show_skeleton: boolean) => { + if (this.skeleton_helper) { + this.skeleton_helper.visible = show_skeleton; this.schedule_render(); } + }; + + private animation_playing_changed = (playing: boolean) => { + if (this.animation) { + this.animation.action.paused = !playing; + + if (playing) { + this.clock.start(); + this.schedule_render(); + } else { + this.clock.stop(); + } + } + }; + + private animation_frame_rate_changed = (frame_rate: number) => { + if (this.animation) { + this.animation.mixer.timeScale = frame_rate / PSO_FRAME_RATE; + } + }; + + private animation_frame_changed = (frame: number) => { + const nj_motion = model_store.current_nj_motion.get(); + + if (this.animation && nj_motion) { + const frame_count = nj_motion.frame_count; + if (frame > frame_count) frame = 1; + if (frame < 1) frame = frame_count; + this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; + this.schedule_render(); + } + }; + + private update_animation_frame(): void { + if (this.animation && !this.animation.action.paused) { + const time = this.animation.action.time; + model_store.animation_frame.set(time * PSO_FRAME_RATE + 1); + } } } diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts deleted file mode 100644 index 09add470..00000000 --- a/src/viewer/rendering/TextureRenderer.ts +++ /dev/null @@ -1,117 +0,0 @@ -import Logger from "js-logger"; -import { autorun } from "mobx"; -import { - Mesh, - MeshBasicMaterial, - OrthographicCamera, - PlaneGeometry, - Texture, - Vector2, - Vector3, -} from "three"; -import { Xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { texture_viewer_store } from "../stores/TextureViewerStore"; -import { Renderer } from "../../core/rendering/Renderer"; -import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; - -const logger = Logger.get("rendering/TextureRenderer"); - -let renderer: TextureRenderer | undefined; - -export function get_texture_renderer(): TextureRenderer { - if (!renderer) renderer = new TextureRenderer(); - return 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; - - 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.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); - } - - 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) { - let tex_3js: Texture | undefined; - - try { - tex_3js = xvm_texture_to_texture(tex); - } catch (e) { - logger.warn("Couldn't convert XVM texture.", e); - } - - const quad_mesh = new Mesh( - this.create_quad( - x, - y + Math.floor((total_height - tex.height) / 2), - tex.width, - tex.height, - ), - tex_3js - ? new MeshBasicMaterial({ - map: tex_3js, - transparent: true, - }) - : new MeshBasicMaterial({ - color: 0xff00ff, - }), - ); - - this.quad_meshes.push(quad_mesh); - this.scene.add(quad_mesh); - - x += 10 + tex.width; - } - }; - - 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; - } -} diff --git a/src/new/viewer/stores/ModelStore.ts b/src/viewer/stores/ModelStore.ts similarity index 88% rename from src/new/viewer/stores/ModelStore.ts rename to src/viewer/stores/ModelStore.ts index 0b288702..d93f4eab 100644 --- a/src/new/viewer/stores/ModelStore.ts +++ b/src/viewer/stores/ModelStore.ts @@ -1,21 +1,20 @@ -import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; -import { Endianness } from "../../../core/data_formats/Endianness"; -import { NjMotion, parse_njm } from "../../../core/data_formats/parsing/ninja/motion"; -import { NjObject, parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja"; +import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; +import { Endianness } from "../../core/data_formats/Endianness"; +import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motion"; +import { NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja"; import { CharacterClassModel } from "../domain/CharacterClassModel"; import { CharacterClassAnimation } from "../domain/CharacterClassAnimation"; import { WritableProperty } from "../../core/observable/WritableProperty"; +import { Disposable } from "../../core/observable/Disposable"; +import { read_file } from "../../core/read_file"; +import { property } from "../../core/observable"; +import { Property } from "../../core/observable/Property"; +import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation"; +import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture"; import { get_character_class_animation_data, get_character_class_data, -} from "../../../viewer/loading/character_class"; -import { Disposable } from "../../core/observable/Disposable"; -import { read_file } from "../../../core/read_file"; -import { property } from "../../core/observable"; -import { Property } from "../../core/observable/Property"; -import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animation"; -import { parse_xvm, Xvm } from "../../../core/data_formats/parsing/ninja/texture"; -import { gui_store, GuiTool } from "../../core/stores/GuiStore"; +} from "../loading/character_class"; import Logger = require("js-logger"); const logger = Logger.get("viewer/stores/ModelStore"); @@ -79,19 +78,6 @@ export class ModelStore implements Disposable { this.current_model.observe(this.load_model), this.current_animation.observe(this.load_animation), ); - - let prev_animation_playing = this.animation_playing.get(); - - this.disposables.push( - gui_store.tool.observe(tool => { - if (tool === GuiTool.Viewer) { - this.animation_playing.set(prev_animation_playing); - } else { - prev_animation_playing = this.animation_playing.get(); - this.animation_playing.set(false); - } - }), - ); } dispose(): void { diff --git a/src/viewer/stores/ModelViewerStore.ts b/src/viewer/stores/ModelViewerStore.ts deleted file mode 100644 index 1ef2bf10..00000000 --- a/src/viewer/stores/ModelViewerStore.ts +++ /dev/null @@ -1,329 +0,0 @@ -import Logger from "js-logger"; -import { action, observable } from "mobx"; -import { - AnimationAction, - AnimationClip, - AnimationMixer, - Clock, - DoubleSide, - Mesh, - MeshLambertMaterial, - SkinnedMesh, - Texture, -} from "three"; -import { Endianness } from "../../core/data_formats/Endianness"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { NjModel, NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ninja"; -import { NjMotion, parse_njm } from "../../core/data_formats/parsing/ninja/motion"; -import { parse_xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { get_character_class_animation_data, get_character_class_data } from "../loading/character_class"; -import { read_file } from "../../core/read_file"; -import { create_skinned_mesh, create_mesh } from "../../core/rendering/conversion/create_mesh"; -import { - create_animation_clip, - PSO_FRAME_RATE, -} from "../../core/rendering/conversion/ninja_animation"; -import { ninja_object_to_buffer_geometry } from "../../core/rendering/conversion/ninja_geometry"; -import { xvm_to_textures } from "../../core/rendering/conversion/ninja_textures"; -import { PlayerAnimation, PlayerModel } from "../domain"; - -const logger = Logger.get("stores/ModelViewerStore"); -const nj_object_cache: Map>> = new Map(); -const nj_motion_cache: Map> = new Map(); - -// TODO: move all Three.js stuff into the renderer. -class ModelViewerStore { - readonly models: PlayerModel[] = [ - new PlayerModel("HUmar", 1, 10, new Set([6])), - new PlayerModel("HUnewearl", 1, 10, new Set()), - new PlayerModel("HUcast", 5, 0, new Set()), - new PlayerModel("HUcaseal", 5, 0, new Set()), - new PlayerModel("RAmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - new PlayerModel("RAmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - new PlayerModel("RAcast", 5, 0, new Set()), - new PlayerModel("RAcaseal", 5, 0, new Set()), - new PlayerModel("FOmar", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - new PlayerModel("FOmarl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - new PlayerModel("FOnewm", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - new PlayerModel("FOnewearl", 1, 10, new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - ]; - readonly animations: PlayerAnimation[] = new Array(572) - .fill(undefined) - .map((_, i) => new PlayerAnimation(i, `Animation ${i + 1}`)); - - readonly clock = new Clock(); - - @observable.ref current_player_model?: PlayerModel; - @observable.ref current_model?: NjObject; - @observable.ref current_bone_count: number = 0; - @observable.ref current_obj3d?: Mesh; - - @observable.ref animation?: { - player_animation?: PlayerAnimation; - 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; - - private has_skeleton = false; - @observable show_skeleton: boolean = false; - - set_animation_frame_rate = action("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 = action("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_model = async (model: PlayerModel) => { - const object = await this.get_player_ninja_object(model); - this.set_model(object, true, model); - // Ignore the bones from the head parts. - this.current_bone_count = 64; - }; - - load_animation = async (animation: PlayerAnimation) => { - const nj_motion = await this.get_nj_motion(animation); - - if (this.current_model) { - this.set_animation(create_animation_clip(this.current_model, 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_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 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 = action("pause_animation", () => { - if (this.animation) { - this.animation.action.paused = true; - this.animation_playing = false; - this.clock.stop(); - } - }); - - toggle_animation_playing = action("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 = action("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 = action("set_animation", (clip: AnimationClip, animation?: PlayerAnimation) => { - 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 set_model = action( - "set_model", - (model: NjObject, skeleton: boolean, player_model?: PlayerModel) => { - if (this.current_obj3d && this.animation) { - this.animation.mixer.stopAllAction(); - this.animation.mixer.uncacheRoot(this.current_obj3d); - this.animation = undefined; - } - - this.current_player_model = player_model; - this.current_model = model; - this.current_bone_count = model.bone_count(); - this.has_skeleton = skeleton; - - this.set_obj3d(); - }, - ); - - 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_player_ninja_object(model: PlayerModel): Promise> { - let ninja_object = nj_object_cache.get(model.name); - - if (ninja_object) { - return ninja_object; - } else { - ninja_object = this.get_all_assets(model); - nj_object_cache.set(model.name, ninja_object); - return ninja_object; - } - } - - private async get_all_assets(model: PlayerModel): Promise> { - const body_data = await get_character_class_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_character_class_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_character_class_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_character_class_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 async get_nj_motion(animation: PlayerAnimation): Promise { - let nj_motion = nj_motion_cache.get(animation.id); - - if (nj_motion) { - return nj_motion; - } else { - nj_motion = get_character_class_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 = action("set_textures", (textures: Texture[]) => { - this.set_obj3d(textures); - }); - - private set_obj3d = (textures?: Texture[]) => { - if (this.current_model) { - let mesh: Mesh; - - const materials = - textures && - textures.map( - tex => - new MeshLambertMaterial({ - skinning: this.has_skeleton, - map: tex, - side: DoubleSide, - alphaTest: 0.5, - }), - ); - - if (this.has_skeleton) { - mesh = create_skinned_mesh( - ninja_object_to_buffer_geometry(this.current_model), - materials, - ); - } else { - mesh = create_mesh(ninja_object_to_buffer_geometry(this.current_model), materials); - } - - // Make sure we rotate around the center of the model. - const bb = mesh.geometry.boundingBox; - const height = bb.max.y - bb.min.y; - mesh.translateY(-height / 2 - bb.min.y); - - this.current_obj3d = mesh; - } - }; -} - -export const model_viewer_store = new ModelViewerStore(); diff --git a/src/viewer/stores/TextureViewerStore.ts b/src/viewer/stores/TextureViewerStore.ts deleted file mode 100644 index 831d6677..00000000 --- a/src/viewer/stores/TextureViewerStore.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { observable } from "mobx"; -import { Xvm, parse_xvm } from "../../core/data_formats/parsing/ninja/texture"; -import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; -import { read_file } from "../../core/read_file"; -import { Endianness } from "../../core/data_formats/Endianness"; -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(); diff --git a/src/viewer/ui/ViewerComponent.css b/src/viewer/ui/ViewerComponent.css deleted file mode 100644 index c5404e12..00000000 --- a/src/viewer/ui/ViewerComponent.css +++ /dev/null @@ -1,23 +0,0 @@ -.main { - display: flex; - padding-top: 10px; - overflow: hidden; -} - -.main > :global(.ant-tabs) { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.main > :global(.ant-tabs > .ant-tabs-content) { - flex: 1; - overflow: hidden; -} - -.main > :global(.ant-tabs > .ant-tabs-content > .ant-tabs-tabpane-active) { - width: 100%; - height: 100%; - overflow: hidden; -} diff --git a/src/viewer/ui/ViewerComponent.tsx b/src/viewer/ui/ViewerComponent.tsx deleted file mode 100644 index 2f6cae8f..00000000 --- a/src/viewer/ui/ViewerComponent.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Tabs } from "antd"; -import React, { Component, ReactNode } from "react"; -import { ModelViewerComponent } from "./models/ModelViewerComponent"; -import { TextureViewerComponent } from "./textures/TextureViewerComponent"; -import styles from "./ViewerComponent.css"; - -export class ViewerComponent extends Component { - render(): ReactNode { - return ( -
- - - - - - - - -
- ); - } -} diff --git a/src/viewer/ui/models/AnimationSelectionComponent.css b/src/viewer/ui/models/AnimationSelectionComponent.css deleted file mode 100644 index 9bebb47c..00000000 --- a/src/viewer/ui/models/AnimationSelectionComponent.css +++ /dev/null @@ -1,25 +0,0 @@ -.main { - margin: 0 10px; -} - -.main > ul { - height: 100%; - padding: 0; - margin: 0; - overflow-y: scroll; - list-style-type: none; -} - -.main > ul > li { - cursor: pointer; - padding: 2px 5px; - white-space: nowrap; -} - -.main > ul > li.selected { - color: var(--hover-color); -} - -.main > ul > li:hover { - color: var(--hover-color); -} diff --git a/src/viewer/ui/models/AnimationSelectionComponent.tsx b/src/viewer/ui/models/AnimationSelectionComponent.tsx deleted file mode 100644 index 12b0dd64..00000000 --- a/src/viewer/ui/models/AnimationSelectionComponent.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Component, ReactNode } from "react"; -import { model_viewer_store } from "../../stores/ModelViewerStore"; -import styles from "./AnimationSelectionComponent.css"; -import { observer } from "mobx-react"; - -@observer -export class AnimationSelectionComponent extends Component { - render(): ReactNode { - return ( -
-
    - {model_viewer_store.animations.map(animation => { - const selected = - model_viewer_store.animation && - model_viewer_store.animation.player_animation && - model_viewer_store.animation.player_animation.id === animation.id; - - return ( -
  • model_viewer_store.load_animation(animation)} - > - {animation.name} -
  • - ); - })} -
-
- ); - } -} diff --git a/src/viewer/ui/models/ModelSelectionComponent.css b/src/viewer/ui/models/ModelSelectionComponent.css deleted file mode 100644 index faede7f0..00000000 --- a/src/viewer/ui/models/ModelSelectionComponent.css +++ /dev/null @@ -1,15 +0,0 @@ -.main { - margin: 0 10px; -} - -.model { - cursor: pointer; -} - -.model.selected { - color: var(--hover-color); -} - -.model:hover { - color: --hover-color; -} diff --git a/src/viewer/ui/models/ModelSelectionComponent.tsx b/src/viewer/ui/models/ModelSelectionComponent.tsx deleted file mode 100644 index 918cdf12..00000000 --- a/src/viewer/ui/models/ModelSelectionComponent.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { List } from "antd"; -import { observer } from "mobx-react"; -import React, { Component, ReactNode } from "react"; -import { model_viewer_store } from "../../stores/ModelViewerStore"; -import styles from "./ModelSelectionComponent.css"; - -@observer -export class ModelSelectionComponent extends Component { - render(): ReactNode { - // Make sure we trigger mobx. - const current = model_viewer_store.current_player_model; - - return ( -
- { - const selected = current === model; - - return ( - model_viewer_store.load_model(model)}> - - {model.name} - - } - /> - - ); - }} - /> -
- ); - } -} diff --git a/src/viewer/ui/models/ModelViewerComponent.css b/src/viewer/ui/models/ModelViewerComponent.css deleted file mode 100644 index dc8dd9b7..00000000 --- a/src/viewer/ui/models/ModelViewerComponent.css +++ /dev/null @@ -1,35 +0,0 @@ -.main { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; -} - -.toolbar { - display: flex; - padding: 10px 5px; - align-items: center; -} - -.toolbar > * { - margin: 0 5px; -} - -.toolbar .group { - display: flex; - align-items: center; -} - -.toolbar .group > * { - margin: 0 5px; -} - -.content { - flex: 1; - display: flex; - overflow: hidden; -} - -.renderer { - flex: 1; -} diff --git a/src/viewer/ui/models/ModelViewerComponent.tsx b/src/viewer/ui/models/ModelViewerComponent.tsx deleted file mode 100644 index 112267df..00000000 --- a/src/viewer/ui/models/ModelViewerComponent.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Button, InputNumber, Switch, Upload } from "antd"; -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 { AutoSizer } from "react-virtualized"; -import { get_model_renderer } from "../../rendering/ModelRenderer"; -import { model_viewer_store } from "../../stores/ModelViewerStore"; -import { RendererComponent } from "../../../core/ui/RendererComponent"; -import { AnimationSelectionComponent } from "./AnimationSelectionComponent"; -import { ModelSelectionComponent } from "./ModelSelectionComponent"; -import styles from "./ModelViewerComponent.css"; - -@observer -export class ModelViewerComponent extends Component { - componentDidMount(): void { - if (!model_viewer_store.current_model) { - model_viewer_store.load_model(model_viewer_store.models[5]); - } - } - - render(): ReactNode { - return ( -
- -
- - -
- - {({ width, height }) => ( - - )} - -
-
-
- ); - } -} - -@observer -class Toolbar extends Component { - state = { - filename: undefined, - }; - - render(): ReactNode { - return ( -
- false} - > - - - {model_viewer_store.animation && ( - <> - -
- Frame rate: - - model_viewer_store.set_animation_frame_rate(value || 0) - } - min={1} - step={1} - /> -
-
- Frame: - - model_viewer_store.set_animation_frame(value || 0) - } - step={1} - /> - / {model_viewer_store.animation_frame_count} -
- - )} -
- Show skeleton: - (model_viewer_store.show_skeleton = value)} - /> -
-
- ); - } - - private load_file = (info: UploadChangeParam) => { - if (info.file.originFileObj) { - this.setState({ filename: info.file.name }); - model_viewer_store.load_file(info.file.originFileObj as File); - } - }; -} diff --git a/src/viewer/ui/textures/TextureViewerComponent.css b/src/viewer/ui/textures/TextureViewerComponent.css deleted file mode 100644 index 603bed20..00000000 --- a/src/viewer/ui/textures/TextureViewerComponent.css +++ /dev/null @@ -1,14 +0,0 @@ -.main { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; -} - -.toolbar { - margin: 10px; -} - -.renderer { - flex: 1; -} diff --git a/src/viewer/ui/textures/TextureViewerComponent.tsx b/src/viewer/ui/textures/TextureViewerComponent.tsx deleted file mode 100644 index 38171c26..00000000 --- a/src/viewer/ui/textures/TextureViewerComponent.tsx +++ /dev/null @@ -1,60 +0,0 @@ -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 "../../../core/ui/RendererComponent"; -import styles from "./TextureViewerComponent.css"; -import { AutoSizer } from "react-virtualized"; - -export class TextureViewerComponent extends Component { - render(): ReactNode { - return ( -
- -
- - {({ width, height }) => ( - - )} - -
-
- ); - } -} - -@observer -class Toolbar extends Component { - state = { - filename: undefined, - }; - - render(): ReactNode { - return ( -
- false} - > - - -
- ); - } - - private load_file = (info: UploadChangeParam) => { - if (info.file.originFileObj) { - this.setState({ filename: info.file.name }); - texture_viewer_store.load_file(info.file.originFileObj as File); - } - }; -} diff --git a/webpack.common.js b/webpack.common.js index 8a23c494..2209700f 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -4,7 +4,7 @@ const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); const { ProvidePlugin } = require("webpack"); module.exports = { - entry: "./src/index.tsx", + entry: "./src/index.ts", output: { path: path.resolve(__dirname, "dist"), }, From c4865ee510f498773175bf37092785dd3dfea503 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 18:17:00 +0200 Subject: [PATCH 08/50] Tweaked TabContainer and ToolBar. --- src/core/gui/TabContainer.css | 10 +++++++--- src/core/gui/ToolBar.css | 1 - src/core/gui/ToolBar.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/gui/TabContainer.css b/src/core/gui/TabContainer.css index cce87e05..2b3c0a48 100644 --- a/src/core/gui/TabContainer.css +++ b/src/core/gui/TabContainer.css @@ -2,14 +2,17 @@ box-sizing: border-box; background-color: hsl(0, 0%, 16%); padding: 3px 0 0 0; + border-bottom: solid 1px var(--border-color); } .core_TabContainer_Tab { + box-sizing: border-box; display: inline-block; - height: 100%; - line-height: 25px; + height: calc(100% + 1px); + line-height: 22px; padding: 0 10px; - margin: 0 1px; + border: solid 1px var(--border-color); + margin: 0 1px -1px 1px; color: #c0c0c0; font-size: 15px; } @@ -22,4 +25,5 @@ .core_TabContainer_Tab.active { background-color: var(--bg-color); color: hsl(0, 0%, 90%); + border-bottom-color: var(--bg-color); } diff --git a/src/core/gui/ToolBar.css b/src/core/gui/ToolBar.css index 439c1b1d..a556c3e3 100644 --- a/src/core/gui/ToolBar.css +++ b/src/core/gui/ToolBar.css @@ -3,7 +3,6 @@ display: flex; flex-direction: row; align-items: center; - padding-top: 1px; border-bottom: solid var(--border-color) 1px; } diff --git a/src/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index 7088114c..5583cbf8 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -5,7 +5,7 @@ import { LabelledControl } from "./LabelledControl"; export class ToolBar extends View { readonly element = create_el("div", "core_ToolBar"); - readonly height = 34; + readonly height = 35; constructor(...children: View[]) { super(); From 844e63735eeb1267ac6be20069cf3838c45b9b63 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 18:59:56 +0200 Subject: [PATCH 09/50] Texture viewer now uses the new UI system. --- src/core/gui/View.ts | 9 +- src/core/observable/Disposer.ts | 23 +++++ src/core/rendering/Renderer.ts | 3 +- src/index.css | 4 +- src/viewer/gui/ModelView.ts | 2 +- src/viewer/gui/TextureView.ts | 46 ++++++++- src/viewer/rendering/ModelRenderer.ts | 7 +- src/viewer/rendering/TextureRenderer.ts | 118 ++++++++++++++++++++++++ src/viewer/stores/TextureStore.ts | 25 +++++ 9 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 src/core/observable/Disposer.ts create mode 100644 src/viewer/rendering/TextureRenderer.ts create mode 100644 src/viewer/stores/TextureStore.ts diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts index 8d75d8ea..cb82e2c8 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/View.ts @@ -1,4 +1,5 @@ import { Disposable } from "../observable/Disposable"; +import { Disposer } from "../observable/Disposer"; export abstract class View implements Disposable { abstract readonly element: HTMLElement; @@ -11,19 +12,19 @@ export abstract class View implements Disposable { this.element.id = id; } - private disposable_list: Disposable[] = []; + private disposer = new Disposer(); protected disposable(disposable: T): T { - this.disposable_list.push(disposable); + this.disposer.add(disposable); return disposable; } protected disposables(...disposables: Disposable[]): void { - this.disposable_list.push(...disposables); + this.disposer.add(...disposables); } dispose(): void { this.element.remove(); - this.disposable_list.splice(0, this.disposable_list.length).forEach(d => d.dispose()); + this.disposer.dispose(); } } diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts new file mode 100644 index 00000000..e3bccc6c --- /dev/null +++ b/src/core/observable/Disposer.ts @@ -0,0 +1,23 @@ +import { Disposable } from "./Disposable"; +import Logger = require("js-logger"); + +const logger = Logger.get("core/observable/Disposer"); + +export class Disposer implements Disposable { + private readonly disposables: Disposable[] = []; + + add(...disposable: Disposable[]): this { + this.disposables.push(...disposable); + return this; + } + + dispose(): void { + for (const disposable of this.disposables.splice(0, this.disposables.length)) { + try { + disposable.dispose(); + } catch (e) { + logger.warn("Error while disposing.", e); + } + } + } +} diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index f0964b03..47522055 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -13,6 +13,7 @@ import { Vector3, WebGLRenderer, } from "three"; +import { Disposable } from "../observable/Disposable"; CameraControls.install({ // Hack to make panning and orbiting work the way we want. @@ -22,7 +23,7 @@ CameraControls.install({ }, }); -export abstract class Renderer { +export abstract class Renderer implements Disposable { protected _debug = false; get debug(): boolean { diff --git a/src/index.css b/src/index.css index 88450b65..7cfe7a41 100644 --- a/src/index.css +++ b/src/index.css @@ -9,8 +9,8 @@ --scrollbar-color: hsl(0, 0%, 17%); --scrollbar-thumb-color: hsl(0, 0%, 23%); - --input-bg-color: hsl(0, 0%, 10%); - --input-bg-color-disabled: hsl(0, 0%, 15%); + --input-bg-color: hsl(0, 0%, 15%); + --input-bg-color-disabled: hsl(0, 0%, 20%); } * { diff --git a/src/viewer/gui/ModelView.ts b/src/viewer/gui/ModelView.ts index d5832be8..a0fc35a5 100644 --- a/src/viewer/gui/ModelView.ts +++ b/src/viewer/gui/ModelView.ts @@ -18,7 +18,7 @@ const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 150; export class ModelView extends ResizableView { - element = create_el("div", "viewer_ModelView"); + readonly element = create_el("div", "viewer_ModelView"); private tool_bar_view = this.disposable(new ToolBarView()); private container_element = create_el("div", "viewer_ModelView_container"); diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index d1fcdd9d..694dddb8 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -1,6 +1,50 @@ import { create_el } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; +import { FileButton } from "../../core/gui/FileButton"; +import { ToolBar } from "../../core/gui/ToolBar"; +import { texture_store } from "../stores/TextureStore"; +import { RendererView } from "../../core/gui/RendererView"; +import { TextureRenderer } from "../rendering/TextureRenderer"; +import { gui_store, GuiTool } from "../../core/stores/GuiStore"; export class TextureView extends ResizableView { - element = create_el("div", "viewer_TextureView", el => (el.textContent = "Texture")); + readonly element = create_el("div", "viewer_TextureView"); + + private readonly open_file_button = new FileButton("Open file...", ".xvm"); + + private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); + + private readonly renderer_view = this.disposable(new RendererView(new TextureRenderer())); + + constructor() { + super(); + + this.element.append(this.tool_bar.element, this.renderer_view.element); + + this.disposable( + this.open_file_button.files.observe(files => { + if (files.length) texture_store.load_file(files[0]); + }), + ); + + this.renderer_view.start_rendering(); + + this.disposable( + gui_store.tool.observe(tool => { + if (tool === GuiTool.Viewer) { + this.renderer_view.start_rendering(); + } else { + this.renderer_view.stop_rendering(); + } + }), + ); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + this.renderer_view.resize(width, Math.max(0, height - this.tool_bar.height)); + + return this; + } } diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index 100ba702..9a634335 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -23,10 +23,11 @@ import { PSO_FRAME_RATE, } from "../../core/rendering/conversion/ninja_animation"; import { Renderer } from "../../core/rendering/Renderer"; +import { Disposer } from "../../core/observable/Disposer"; export class ModelRenderer extends Renderer implements Disposable { private readonly perspective_camera: PerspectiveCamera; - private readonly disposables: Disposable[] = []; + private readonly disposer = new Disposer(); private readonly clock = new Clock(); private mesh?: Object3D; private skeleton_helper?: SkeletonHelper; @@ -41,7 +42,7 @@ export class ModelRenderer extends Renderer implements Disposable { this.perspective_camera = this.camera as PerspectiveCamera; - this.disposables.push( + this.disposer.add( model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), model_store.current_xvm.observe(this.nj_data_or_xvm_changed), model_store.current_nj_motion.observe(this.nj_motion_changed), @@ -60,7 +61,7 @@ export class ModelRenderer extends Renderer implements Disposable { dispose(): void { super.dispose(); - this.disposables.forEach(d => d.dispose()); + this.disposer.dispose(); } protected render(): void { diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts new file mode 100644 index 00000000..12cd4f00 --- /dev/null +++ b/src/viewer/rendering/TextureRenderer.ts @@ -0,0 +1,118 @@ +import { + Mesh, + MeshBasicMaterial, + OrthographicCamera, + PlaneGeometry, + Texture, + Vector2, + Vector3, +} from "three"; +import { Disposable } from "../../core/observable/Disposable"; +import { Renderer } from "../../core/rendering/Renderer"; +import { Disposer } from "../../core/observable/Disposer"; +import { Xvm } from "../../core/data_formats/parsing/ninja/texture"; +import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures"; +import Logger = require("js-logger"); +import { texture_store } from "../stores/TextureStore"; + +const logger = Logger.get("viewer/rendering/TextureRenderer"); + +export class TextureRenderer extends Renderer implements Disposable { + private readonly ortho_camera: OrthographicCamera; + private readonly disposer = new Disposer(); + private readonly quad_meshes: Mesh[] = []; + + constructor() { + super(new OrthographicCamera(-400, 400, 300, -300, 1, 10)); + + this.ortho_camera = this.camera as OrthographicCamera; + this.controls.dollySpeed = -1; + + this.controls.azimuthRotateSpeed = 0; + this.controls.polarRotateSpeed = 0; + + this.disposer.add( + texture_store.current_xvm.observe(xvm => { + this.scene.remove(...this.quad_meshes); + + 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.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); + } + + dispose(): void { + super.dispose(); + this.disposer.dispose(); + } + + private render_textures(xvm: Xvm): void { + 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) { + let tex_3js: Texture | undefined; + + try { + tex_3js = xvm_texture_to_texture(tex); + } catch (e) { + logger.warn("Couldn't convert XVM texture.", e); + } + + const quad_mesh = new Mesh( + this.create_quad( + x, + y + Math.floor((total_height - tex.height) / 2), + tex.width, + tex.height, + ), + tex_3js + ? new MeshBasicMaterial({ + map: tex_3js, + transparent: true, + }) + : new MeshBasicMaterial({ + color: 0xff00ff, + }), + ); + + this.quad_meshes.push(quad_mesh); + this.scene.add(quad_mesh); + + x += 10 + tex.width; + } + } + + 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; + } +} diff --git a/src/viewer/stores/TextureStore.ts b/src/viewer/stores/TextureStore.ts new file mode 100644 index 00000000..9eed29b2 --- /dev/null +++ b/src/viewer/stores/TextureStore.ts @@ -0,0 +1,25 @@ +import { property } from "../../core/observable"; +import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture"; +import { Property } from "../../core/observable/Property"; +import { read_file } from "../../core/read_file"; +import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; +import { Endianness } from "../../core/data_formats/Endianness"; +import Logger = require("js-logger"); + +const logger = Logger.get("viewer/stores/TextureStore"); + +export class TextureStore { + private readonly _current_xvm = property(undefined); + readonly current_xvm: Property = this._current_xvm; + + load_file = async (file: File) => { + try { + const buffer = await read_file(file); + this._current_xvm.set(parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little))); + } catch (e) { + logger.error("Couldn't read file.", e); + } + }; +} + +export const texture_store = new TextureStore(); From 3185737cd9f38fd524ccd0e950caca4b9ed6020d Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 19:06:02 +0200 Subject: [PATCH 10/50] Made fonts slightly smaller. --- src/index.css | 5 +---- src/viewer/gui/ModelView.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/index.css b/src/index.css index 7cfe7a41..4557ac72 100644 --- a/src/index.css +++ b/src/index.css @@ -15,10 +15,6 @@ * { 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 { @@ -42,6 +38,7 @@ body { user-select: none; overflow: hidden; margin: 0; + font-size: 15px; font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; background-color: var(--bg-color); diff --git a/src/viewer/gui/ModelView.ts b/src/viewer/gui/ModelView.ts index a0fc35a5..6bd64476 100644 --- a/src/viewer/gui/ModelView.ts +++ b/src/viewer/gui/ModelView.ts @@ -15,7 +15,7 @@ import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation"; const MODEL_LIST_WIDTH = 100; -const ANIMATION_LIST_WIDTH = 150; +const ANIMATION_LIST_WIDTH = 130; export class ModelView extends ResizableView { readonly element = create_el("div", "viewer_ModelView"); From dbd5dba68277c2a28eb6e7891b806adab90b9a72 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 22:04:08 +0200 Subject: [PATCH 11/50] Tweaked theme and started working on porting quest editor to the new UI system. --- assets_generation/update_drops_ephinea.ts | 2 +- assets_generation/update_ephinea_data.ts | 2 +- src/application/gui/MainContentView.ts | 8 +- src/application/gui/NavigationView.css | 15 ++-- src/application/gui/NavigationView.ts | 2 +- src/{old => }/core/domain/index.ts | 2 +- src/core/gui/Button.css | 33 ++++--- src/core/gui/Button.ts | 5 +- src/core/gui/FileButton.ts | 6 +- src/core/gui/Input.css | 29 +++--- src/core/gui/NumberInput.css | 2 +- src/core/gui/NumberInput.ts | 36 +++++--- src/core/gui/TabContainer.css | 4 +- src/core/gui/ToolBar.css | 19 +--- src/core/gui/ToolBar.ts | 2 +- src/core/gui/View.ts | 5 +- src/core/observable/Disposer.ts | 7 +- src/{old => }/core/persistence.ts | 2 +- src/index.css | 15 ++-- src/old/core/stores/ItemTypeStore.ts | 2 +- src/old/core/stores/ServerMap.ts | 2 +- src/old/core/ui/SectionIdIcon.tsx | 2 +- src/old/hunt_optimizer/domain/index.ts | 2 +- .../persistence/HuntMethodPersister.ts | 4 +- .../persistence/HuntOptimizerPersister.ts | 4 +- .../hunt_optimizer/stores/HuntMethodStore.ts | 2 +- .../stores/HuntOptimizerStore.ts | 2 +- .../hunt_optimizer/stores/ItemDropStore.ts | 2 +- .../ui/OptimizationResultComponent.tsx | 2 +- .../quest_editor/ui/QuestEditorComponent.tsx | 2 +- src/quest_editor/gui/QuestEditorView.ts | 88 +++++++++++++++++++ src/quest_editor/gui/ToolBarView.ts | 24 +++++ .../persistence/QuestEditorUiPersister.ts | 0 src/viewer/gui/ModelView.css | 8 +- src/viewer/rendering/ModelRenderer.ts | 2 +- src/viewer/rendering/TextureRenderer.ts | 2 +- 36 files changed, 242 insertions(+), 104 deletions(-) rename src/{old => }/core/domain/index.ts (91%) rename src/{old => }/core/persistence.ts (94%) create mode 100644 src/quest_editor/gui/QuestEditorView.ts create mode 100644 src/quest_editor/gui/ToolBarView.ts rename src/{old => }/quest_editor/persistence/QuestEditorUiPersister.ts (100%) diff --git a/assets_generation/update_drops_ephinea.ts b/assets_generation/update_drops_ephinea.ts index c2cc003e..b6dd9567 100644 --- a/assets_generation/update_drops_ephinea.ts +++ b/assets_generation/update_drops_ephinea.ts @@ -3,7 +3,7 @@ import { writeFileSync } from "fs"; import "isomorphic-fetch"; import Logger from "js-logger"; import { ASSETS_DIR } from "."; -import { Difficulty, SectionId, SectionIds } from "../src/old/core/domain"; +import { Difficulty, SectionId, SectionIds } from "../src/core/domain"; import { BoxDropDto, EnemyDropDto, ItemTypeDto } from "../src/old/core/dto"; import { name_and_episode_to_npc_type, diff --git a/assets_generation/update_ephinea_data.ts b/assets_generation/update_ephinea_data.ts index a3021317..bc1ce527 100644 --- a/assets_generation/update_ephinea_data.ts +++ b/assets_generation/update_ephinea_data.ts @@ -5,7 +5,7 @@ import { BufferCursor } from "../src/core/data_formats/cursor/BufferCursor"; import { ItemPmt, parse_item_pmt } from "../src/core/data_formats/parsing/itempmt"; import { parse_quest } from "../src/core/data_formats/parsing/quest"; import { parse_unitxt, Unitxt } from "../src/core/data_formats/parsing/unitxt"; -import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/old/core/domain"; +import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/core/domain"; import { BoxDropDto, EnemyDropDto, ItemTypeDto, QuestDto } from "../src/old/core/dto"; import { update_drops_from_website } from "./update_drops_ephinea"; import { Episode, EPISODES } from "../src/core/data_formats/parsing/quest/Episode"; diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 48e6dc79..4e6cf3f3 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,12 +1,14 @@ 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][] = [ +const TOOLS: [GuiTool, () => Promise][] = [ [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], + [ + GuiTool.QuestEditor, + async () => new (await import("../../quest_editor/gui/QuestEditorView")).QuestEditorView(), + ], ]; export class MainContentView extends ResizableView { diff --git a/src/application/gui/NavigationView.css b/src/application/gui/NavigationView.css index c2a752e0..cb8cf677 100644 --- a/src/application/gui/NavigationView.css +++ b/src/application/gui/NavigationView.css @@ -3,7 +3,7 @@ display: flex; flex-direction: row; align-items: stretch; - background-color: hsl(0, 0%, 12%); + background-color: hsl(0, 0%, 10%); border-bottom: solid 2px var(--bg-color); } @@ -14,18 +14,19 @@ .application_ToolButton label { box-sizing: border-box; display: inline-block; - font-size: 16px; + font-size: 15px; height: 100%; padding: 0 20px; - line-height: 40px; + line-height: 29px; + color: hsl(0, 0%, 65%); } .application_ToolButton label:hover { - color: hsl(200, 25%, 85%); - background-color: hsl(0, 0%, 16%); + color: hsl(0, 0%, 85%); + background-color: hsl(0, 0%, 12%); } .application_ToolButton input:checked + label { - color: hsl(200, 50%, 85%); - background-color: hsl(0, 0%, 20%); + color: hsl(0, 0%, 85%); + background-color: var(--bg-color); } diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 25f5814a..ab28b27f 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -11,7 +11,7 @@ const TOOLS: [GuiTool, string][] = [ export class NavigationView extends View { element = create_el("div", "application_NavigationView"); - height = 40; + height = 30; private buttons = new Map( TOOLS.map(([value, text]) => [value, this.disposable(new ToolButton(value, text))]), diff --git a/src/old/core/domain/index.ts b/src/core/domain/index.ts similarity index 91% rename from src/old/core/domain/index.ts rename to src/core/domain/index.ts index 517ae122..804a058c 100644 --- a/src/old/core/domain/index.ts +++ b/src/core/domain/index.ts @@ -1,4 +1,4 @@ -import { enum_values } from "../../../core/enums"; +import { enum_values } from "../enums"; export const RARE_ENEMY_PROB = 1 / 512; export const KONDRIEU_PROB = 1 / 10; diff --git a/src/core/gui/Button.css b/src/core/gui/Button.css index 0c9e08ce..4e7b50d0 100644 --- a/src/core/gui/Button.css +++ b/src/core/gui/Button.css @@ -1,21 +1,30 @@ .core_Button { display: inline-block; box-sizing: border-box; - background-color: #404040; - height: 26px; - padding: 2px 8px; - border: solid 1px #606060; - color: #f0f0f0; + padding: 0; + border: solid 1px hsl(0, 0%, 10%); + color: hsl(0, 0%, 80%); outline: none; } -.core_Button:hover { - background-color: #505050; - border-color: #707070; +.core_Button .core_Button_inner { + display: inline-block; + box-sizing: border-box; + background-color: hsl(0, 0%, 20%); + height: 24px; + line-height: 17px; + padding: 3px 8px; + border: solid 1px hsl(0, 0%, 35%); } -.core_Button:active { - background-color: #404040; - border-color: #606060; - color: #e0e0e0; +.core_Button:hover .core_Button_inner { + background-color: hsl(0, 0%, 25%); + border-color: hsl(0, 0%, 40%); + color: hsl(0, 0%, 90%); +} + +.core_Button:active .core_Button_inner { + background-color: hsl(0, 0%, 20%); + border-color: hsl(0, 0%, 30%); + color: hsl(0, 0%, 75%); } diff --git a/src/core/gui/Button.ts b/src/core/gui/Button.ts index 2becce54..aa0c0856 100644 --- a/src/core/gui/Button.ts +++ b/src/core/gui/Button.ts @@ -13,7 +13,10 @@ export class Button extends View { constructor(text: string) { super(); - this.element.textContent = text; + const inner_element = create_el("span", "core_Button_inner"); + inner_element.textContent = text; + + this.element.append(inner_element); this.element.onclick = (e: MouseEvent) => this._click.emit(e, undefined); } diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index 3749b4e3..0b20c0d2 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -26,7 +26,9 @@ export class FileButton extends View { } }; - this.element.textContent = text; - this.element.append(this.input); + const inner_element = create_el("span", "core_FileButton_inner core_Button_inner"); + inner_element.textContent = text; + + this.element.append(inner_element, this.input); } } diff --git a/src/core/gui/Input.css b/src/core/gui/Input.css index dbb85b0d..39797b95 100644 --- a/src/core/gui/Input.css +++ b/src/core/gui/Input.css @@ -1,23 +1,32 @@ .core_Input { box-sizing: border-box; - height: 26px; + border: solid 1px hsl(0, 0%, 25%); +} + +.core_Input .core_Input_inner { + box-sizing: border-box; + width: 100%; + height: 24px; padding: 0 3px; - border: solid 1px var(--border-color); - background-color: var(--input-bg-color); - color: var(--text-color); + border: solid 1px hsl(0, 0%, 0%); + background-color: hsl(0, 0%, 12%); + color: hsl(0, 0%, 75%); outline: none; } .core_Input:hover { - border: solid 1px var(--border-color-hover); + border-color: hsl(0, 0%, 35%); } -.core_Input:focus { - border: solid 1px var(--border-color-focus); +.core_Input:focus-within { + border-color: hsl(0, 0%, 45%); } -.core_Input:disabled { +.core_Input.disabled { + border: solid 1px hsl(0, 0%, 20%); +} + +.core_Input.disabled .core_Input_inner { + background-color: hsl(0, 0%, 15%); color: var(--text-color-disabled); - background-color: var(--input-bg-color-disabled); - border: solid 1px var(--border-color); } diff --git a/src/core/gui/NumberInput.css b/src/core/gui/NumberInput.css index 3f9981f0..b5e37ab0 100644 --- a/src/core/gui/NumberInput.css +++ b/src/core/gui/NumberInput.css @@ -1,3 +1,3 @@ -.core_NumberInput { +.core_NumberInput .core_NumberInput_inner { text-align: right; } \ No newline at end of file diff --git a/src/core/gui/NumberInput.ts b/src/core/gui/NumberInput.ts index 347232ea..9700cb02 100644 --- a/src/core/gui/NumberInput.ts +++ b/src/core/gui/NumberInput.ts @@ -7,12 +7,17 @@ import { LabelledControl } from "./LabelledControl"; import { is_any_property, Property } from "../observable/Property"; export class NumberInput extends LabelledControl { - readonly element: HTMLInputElement = create_el("input", "core_NumberInput core_Input"); + readonly element = create_el("span", "core_NumberInput core_Input"); readonly value: WritableProperty = property(0); readonly preferred_label_position = "left"; + private readonly input: HTMLInputElement = create_el( + "input", + "core_NumberInput_inner core_Input_inner", + ); + constructor( value = 0, label?: string, @@ -22,29 +27,40 @@ export class NumberInput extends LabelledControl { ) { super(label); - this.element.type = "number"; - this.element.valueAsNumber = value; - this.element.style.width = "50px"; + this.input.type = "number"; + this.input.valueAsNumber = value; this.set_prop("min", min); this.set_prop("max", max); this.set_prop("step", step); - this.element.onchange = () => this.value.set(this.element.valueAsNumber); + this.input.onchange = () => this.value.set(this.input.valueAsNumber); + + this.element.append(this.input); this.disposables( - this.value.observe(value => (this.element.valueAsNumber = value)), + this.value.observe(value => (this.input.valueAsNumber = value)), - this.enabled.observe(enabled => (this.element.disabled = !enabled)), + this.enabled.observe(enabled => { + this.input.disabled = !enabled; + + if (enabled) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } + }), ); + + this.element.style.width = "50px"; } private set_prop(prop: "min" | "max" | "step", value: T | Property): void { if (is_any_property(value)) { - this.element[prop] = String(value.get()); - this.disposable(value.observe(v => (this.element[prop] = String(v)))); + this.input[prop] = String(value.get()); + this.disposable(value.observe(v => (this.input[prop] = String(v)))); } else { - this.element[prop] = String(value); + this.input[prop] = String(value); } } } diff --git a/src/core/gui/TabContainer.css b/src/core/gui/TabContainer.css index 2b3c0a48..68c14863 100644 --- a/src/core/gui/TabContainer.css +++ b/src/core/gui/TabContainer.css @@ -1,6 +1,5 @@ .core_TabContainer_Bar { box-sizing: border-box; - background-color: hsl(0, 0%, 16%); padding: 3px 0 0 0; border-bottom: solid 1px var(--border-color); } @@ -13,7 +12,8 @@ padding: 0 10px; border: solid 1px var(--border-color); margin: 0 1px -1px 1px; - color: #c0c0c0; + background-color: hsl(0, 0%, 12%); + color: hsl(0, 0%, 75%); font-size: 15px; } diff --git a/src/core/gui/ToolBar.css b/src/core/gui/ToolBar.css index a556c3e3..d629ebd1 100644 --- a/src/core/gui/ToolBar.css +++ b/src/core/gui/ToolBar.css @@ -7,10 +7,11 @@ } .core_ToolBar > * { - margin: 2px 4px; + margin: 2px 1px; } .core_ToolBar > .core_ToolBar_group { + margin: 2px 3px; display: flex; flex-direction: row; align-items: center; @@ -19,19 +20,3 @@ .core_ToolBar > .core_ToolBar_group > * { margin: 0 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/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index 5583cbf8..009d41d4 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -5,7 +5,7 @@ import { LabelledControl } from "./LabelledControl"; export class ToolBar extends View { readonly element = create_el("div", "core_ToolBar"); - readonly height = 35; + readonly height = 33; constructor(...children: View[]) { super(); diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts index cb82e2c8..c9101580 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/View.ts @@ -15,12 +15,11 @@ export abstract class View implements Disposable { private disposer = new Disposer(); protected disposable(disposable: T): T { - this.disposer.add(disposable); - return disposable; + return this.disposer.add(disposable); } protected disposables(...disposables: Disposable[]): void { - this.disposer.add(...disposables); + this.disposer.add_all(...disposables); } dispose(): void { diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index e3bccc6c..fd15e14a 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -6,7 +6,12 @@ const logger = Logger.get("core/observable/Disposer"); export class Disposer implements Disposable { private readonly disposables: Disposable[] = []; - add(...disposable: Disposable[]): this { + add(disposable: T): T { + this.disposables.push(disposable); + return disposable; + } + + add_all(...disposable: Disposable[]): this { this.disposables.push(...disposable); return this; } diff --git a/src/old/core/persistence.ts b/src/core/persistence.ts similarity index 94% rename from src/old/core/persistence.ts rename to src/core/persistence.ts index ad255b1e..f8200a6d 100644 --- a/src/old/core/persistence.ts +++ b/src/core/persistence.ts @@ -1,7 +1,7 @@ import Logger from "js-logger"; import { Server } from "./domain"; -const logger = Logger.get("persistence/Persister"); +const logger = Logger.get("core/persistence/Persister"); export abstract class Persister { protected persist_for_server(server: Server, key: string, data: any): void { diff --git a/src/index.css b/src/index.css index 4557ac72..b8475e89 100644 --- a/src/index.css +++ b/src/index.css @@ -1,16 +1,11 @@ :root { - --bg-color: hsl(0, 0%, 20%); - --text-color: hsl(0, 0%, 85%); + --bg-color: hsl(0, 0%, 15%); + --text-color: hsl(0, 0%, 80%); --text-color-disabled: hsl(0, 0%, 55%); - --border-color: hsl(0, 0%, 30%); - --border-color-hover: hsl(0, 0%, 40%); - --border-color-focus: hsl(0, 0%, 50%); + --border-color: hsl(0, 0%, 25%); - --scrollbar-color: hsl(0, 0%, 17%); - --scrollbar-thumb-color: hsl(0, 0%, 23%); - - --input-bg-color: hsl(0, 0%, 15%); - --input-bg-color-disabled: hsl(0, 0%, 20%); + --scrollbar-color: hsl(0, 0%, 13%); + --scrollbar-thumb-color: hsl(0, 0%, 17%); } * { diff --git a/src/old/core/stores/ItemTypeStore.ts b/src/old/core/stores/ItemTypeStore.ts index 7457c9ae..9458064a 100644 --- a/src/old/core/stores/ItemTypeStore.ts +++ b/src/old/core/stores/ItemTypeStore.ts @@ -10,7 +10,7 @@ import { import { Loadable } from "../Loadable"; import { ServerMap } from "./ServerMap"; import { ItemTypeDto } from "../dto"; -import { Server } from "../domain"; +import { Server } from "../../../core/domain"; export class ItemTypeStore { private id_to_item_type: ItemType[] = []; diff --git a/src/old/core/stores/ServerMap.ts b/src/old/core/stores/ServerMap.ts index 4eb17802..081fafb6 100644 --- a/src/old/core/stores/ServerMap.ts +++ b/src/old/core/stores/ServerMap.ts @@ -1,5 +1,5 @@ import { computed } from "mobx"; -import { Server } from "../domain"; +import { Server } from "../../../core/domain"; import { EnumMap } from "../../../core/enums"; /** diff --git a/src/old/core/ui/SectionIdIcon.tsx b/src/old/core/ui/SectionIdIcon.tsx index c42cd34f..2e065305 100644 --- a/src/old/core/ui/SectionIdIcon.tsx +++ b/src/old/core/ui/SectionIdIcon.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { SectionId } from "../domain"; +import { SectionId } from "../../../core/domain"; export function SectionIdIcon({ section_id, diff --git a/src/old/hunt_optimizer/domain/index.ts b/src/old/hunt_optimizer/domain/index.ts index c5c22f9e..c17b1227 100644 --- a/src/old/hunt_optimizer/domain/index.ts +++ b/src/old/hunt_optimizer/domain/index.ts @@ -2,7 +2,7 @@ import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; import { computed, observable } from "mobx"; import { ItemType } from "../../core/domain/items"; -import { Difficulty, SectionId } from "../../core/domain"; +import { Difficulty, SectionId } from "../../../core/domain"; export class HuntMethod { readonly id: string; diff --git a/src/old/hunt_optimizer/persistence/HuntMethodPersister.ts b/src/old/hunt_optimizer/persistence/HuntMethodPersister.ts index 8f4bba83..9914b2dd 100644 --- a/src/old/hunt_optimizer/persistence/HuntMethodPersister.ts +++ b/src/old/hunt_optimizer/persistence/HuntMethodPersister.ts @@ -1,5 +1,5 @@ -import { Persister } from "../../core/persistence"; -import { Server } from "../../core/domain"; +import { Persister } from "../../../core/persistence"; +import { Server } from "../../../core/domain"; import { HuntMethod } from "../domain"; const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes"; diff --git a/src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts b/src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts index 5eb5ff5f..9d728dd3 100644 --- a/src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts +++ b/src/old/hunt_optimizer/persistence/HuntOptimizerPersister.ts @@ -1,7 +1,7 @@ -import { Server } from "../../core/domain"; +import { Server } from "../../../core/domain"; import { WantedItem } from "../stores/HuntOptimizerStore"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; -import { Persister } from "../../core/persistence"; +import { Persister } from "../../../core/persistence"; const WANTED_ITEMS_KEY = "HuntOptimizerStore.wantedItems"; diff --git a/src/old/hunt_optimizer/stores/HuntMethodStore.ts b/src/old/hunt_optimizer/stores/HuntMethodStore.ts index be4874b5..a3828598 100644 --- a/src/old/hunt_optimizer/stores/HuntMethodStore.ts +++ b/src/old/hunt_optimizer/stores/HuntMethodStore.ts @@ -1,6 +1,6 @@ import Logger from "js-logger"; import { autorun, IReactionDisposer, observable } from "mobx"; -import { Server } from "../../core/domain"; +import { Server } from "../../../core/domain"; import { QuestDto } from "../../core/dto"; import { Loadable } from "../../core/Loadable"; import { hunt_method_persister } from "../persistence/HuntMethodPersister"; diff --git a/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts b/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts index c22e3b49..a0699bc9 100644 --- a/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/old/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -8,7 +8,7 @@ import { SectionId, SectionIds, Server, -} from "../../core/domain"; +} from "../../../core/domain"; import { hunt_optimizer_persister } from "../persistence/HuntOptimizerPersister"; import { hunt_method_store } from "./HuntMethodStore"; import { item_drop_stores } from "./ItemDropStore"; diff --git a/src/old/hunt_optimizer/stores/ItemDropStore.ts b/src/old/hunt_optimizer/stores/ItemDropStore.ts index 45cae96a..bdcf06e6 100644 --- a/src/old/hunt_optimizer/stores/ItemDropStore.ts +++ b/src/old/hunt_optimizer/stores/ItemDropStore.ts @@ -1,5 +1,5 @@ import { observable } from "mobx"; -import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/domain"; +import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../../core/domain"; import { EnemyDropDto } from "../../core/dto"; import { Loadable } from "../../core/Loadable"; import { item_type_stores } from "../../core/stores/ItemTypeStore"; diff --git a/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx index 217d1ea1..2fcdc0c1 100644 --- a/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx +++ b/src/old/hunt_optimizer/ui/OptimizationResultComponent.tsx @@ -2,7 +2,7 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import React, { Component, ReactNode } from "react"; import { AutoSizer, Index } from "react-virtualized"; -import { Difficulty, SectionId } from "../../core/domain"; +import { Difficulty, SectionId } from "../../../core/domain"; import { hunt_optimizer_store, OptimalMethod } from "../stores/HuntOptimizerStore"; import { BigTable, Column } from "../../core/ui/BigTable"; import { SectionIdIcon } from "../../core/ui/SectionIdIcon"; diff --git a/src/old/quest_editor/ui/QuestEditorComponent.tsx b/src/old/quest_editor/ui/QuestEditorComponent.tsx index 5a4f7a71..a71bb56d 100644 --- a/src/old/quest_editor/ui/QuestEditorComponent.tsx +++ b/src/old/quest_editor/ui/QuestEditorComponent.tsx @@ -2,7 +2,7 @@ import GoldenLayout, { ContentItem, ItemConfigType } from "golden-layout"; import Logger from "js-logger"; import { observer } from "mobx-react"; import React, { Component, createRef, FocusEvent, ReactNode } from "react"; -import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; +import { quest_editor_ui_persister } from "../../../quest_editor/persistence/QuestEditorUiPersister"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { AssemblyEditorComponent } from "./AssemblyEditorComponent"; import { EntityInfoComponent } from "./EntityInfoComponent"; diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts new file mode 100644 index 00000000..9cdeaf39 --- /dev/null +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -0,0 +1,88 @@ +import { ResizableView } from "../../core/gui/ResizableView"; +import { create_el } from "../../core/gui/dom"; +import { ToolBarView } from "./ToolBarView"; +import GoldenLayout, { ContentItem } from "golden-layout"; +import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; +import { AssemblyEditorComponent } from "../../old/quest_editor/ui/AssemblyEditorComponent"; +import { quest_editor_store } from "../../old/quest_editor/stores/QuestEditorStore"; +import Logger = require("js-logger"); + +const logger = Logger.get("quest_editor/gui/QuestEditorView"); + +const DEFAULT_LAYOUT_CONFIG = { + settings: { + showPopoutIcon: false, + }, + dimensions: { + headerHeight: 28, + }, + labels: { + close: "Close", + maximise: "Maximise", + minimise: "Minimise", + popout: "Open in new window", + }, +}; + +export class QuestEditorView extends ResizableView { + readonly element = create_el("div"); + + private readonly tool_bar_view = this.disposable(new ToolBarView()); + + private layout_element = create_el("div"); + // private layout: GoldenLayout; + + constructor() { + super(); + + // const content = await quest_editor_ui_persister.load_layout_config( + // [...CMP_TO_NAME.values()], + // DEFAULT_LAYOUT_CONTENT, + // ); + // + // const config: GoldenLayout.Config = { + // ...DEFAULT_LAYOUT_CONFIG, + // content, + // }; + // + // try { + // this.layout = new GoldenLayout(config, this.layout_element); + // } catch (e) { + // logger.warn("Couldn't initialize golden layout with persisted layout.", e); + // + // this.layout = new GoldenLayout( + // { + // ...DEFAULT_LAYOUT_CONFIG, + // content: DEFAULT_LAYOUT_CONTENT, + // }, + // this.layout_element, + // ); + // } + // + // for (const [component, name] of CMP_TO_NAME) { + // this.layout.registerComponent(name, component); + // } + // + // this.layout.on("stateChanged", () => { + // if (this.layout) { + // quest_editor_ui_persister.persist_layout_config(this.layout.toConfig().content); + // } + // }); + // + // this.layout.on("stackCreated", (stack: ContentItem) => { + // stack.on("activeContentItemChanged", (item: ContentItem) => { + // if ("component" in item.config) { + // if (item.config.component === CMP_TO_NAME.get(AssemblyEditorComponent)) { + // quest_editor_store.script_undo.make_current(); + // } else { + // quest_editor_store.undo.make_current(); + // } + // } + // }); + // }); + // + // this.layout.init(); + + this.element.append(this.tool_bar_view.element, this.layout_element); + } +} diff --git a/src/quest_editor/gui/ToolBarView.ts b/src/quest_editor/gui/ToolBarView.ts new file mode 100644 index 00000000..a885fa8e --- /dev/null +++ b/src/quest_editor/gui/ToolBarView.ts @@ -0,0 +1,24 @@ +import { View } from "../../core/gui/View"; +import { ToolBar } from "../../core/gui/ToolBar"; +import { FileButton } from "../../core/gui/FileButton"; +import { Button } from "../../core/gui/Button"; + +export class ToolBarView extends View { + private readonly open_file_button = new FileButton("Open file...", ".qst"); + private readonly save_as_button = new Button("Save as..."); + private readonly undo_button = new Button("Undo"); + private readonly redo_button = new Button("Redo"); + + private readonly tool_bar = new ToolBar( + this.open_file_button, + this.save_as_button, + this.undo_button, + this.redo_button, + ); + + readonly element = this.tool_bar.element; + + get height(): number { + return this.tool_bar.height; + } +} diff --git a/src/old/quest_editor/persistence/QuestEditorUiPersister.ts b/src/quest_editor/persistence/QuestEditorUiPersister.ts similarity index 100% rename from src/old/quest_editor/persistence/QuestEditorUiPersister.ts rename to src/quest_editor/persistence/QuestEditorUiPersister.ts diff --git a/src/viewer/gui/ModelView.css b/src/viewer/gui/ModelView.css index 39644d55..7702f1b6 100644 --- a/src/viewer/gui/ModelView.css +++ b/src/viewer/gui/ModelView.css @@ -16,11 +16,11 @@ } .viewer_ModelSelectListView li:hover { - color: hsl(200, 25%, 85%); - background-color: hsl(0, 0%, 25%); + color: hsl(0, 0%, 90%); + background-color: hsl(0, 0%, 18%); } .viewer_ModelSelectListView li.active { - color: hsl(200, 50%, 85%); - background-color: hsl(0, 0%, 30%); + color: hsl(0, 0%, 90%); + background-color: hsl(0, 0%, 21%); } diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index 9a634335..3a7e4116 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -42,7 +42,7 @@ export class ModelRenderer extends Renderer implements Disposable { this.perspective_camera = this.camera as PerspectiveCamera; - this.disposer.add( + this.disposer.add_all( model_store.current_nj_data.observe(this.nj_data_or_xvm_changed), model_store.current_xvm.observe(this.nj_data_or_xvm_changed), model_store.current_nj_motion.observe(this.nj_motion_changed), diff --git a/src/viewer/rendering/TextureRenderer.ts b/src/viewer/rendering/TextureRenderer.ts index 12cd4f00..73ede7bb 100644 --- a/src/viewer/rendering/TextureRenderer.ts +++ b/src/viewer/rendering/TextureRenderer.ts @@ -31,7 +31,7 @@ export class TextureRenderer extends Renderer implements Disposable { this.controls.azimuthRotateSpeed = 0; this.controls.polarRotateSpeed = 0; - this.disposer.add( + this.disposer.add_all( texture_store.current_xvm.observe(xvm => { this.scene.remove(...this.quad_meshes); From 18a8ac1ad697fef0ef5eb6376e82eee784ded8cf Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 21 Aug 2019 22:29:20 +0200 Subject: [PATCH 12/50] Fixed bug in code that starts/stops renderers when not visible. --- src/application/gui/NavigationView.ts | 5 +++-- src/core/rendering/Renderer.ts | 16 +++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index ab28b27f..5dfe0e45 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -10,8 +10,9 @@ const TOOLS: [GuiTool, string][] = [ ]; export class NavigationView extends View { - element = create_el("div", "application_NavigationView"); - height = 30; + readonly element = create_el("div", "application_NavigationView"); + + readonly height = 30; private buttons = new Map( TOOLS.map(([value, text]) => [value, this.disposable(new ToolButton(value, text))]), diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 47522055..27709740 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -41,7 +41,7 @@ export abstract class Renderer implements Disposable { private renderer = new WebGLRenderer({ antialias: true }); private render_scheduled = false; - private render_stop_scheduled = false; + private animation_frame_handle?: number = undefined; private light = new HemisphereLight(0xffffff, 0x505050, 1.2); private controls_clock = new Clock(); @@ -81,11 +81,14 @@ export abstract class Renderer implements Disposable { start_rendering(): void { this.schedule_render(); - requestAnimationFrame(this.call_render); + this.animation_frame_handle = requestAnimationFrame(this.call_render); } stop_rendering(): void { - this.render_stop_scheduled = true; + if (this.animation_frame_handle != undefined) { + cancelAnimationFrame(this.animation_frame_handle); + this.animation_frame_handle = undefined; + } } schedule_render = () => { @@ -121,15 +124,10 @@ export abstract class Renderer implements Disposable { this.render_scheduled = false; - if (this.render_stop_scheduled) { - this.render_stop_scheduled = false; - return; - } - if (should_render) { this.render(); } - requestAnimationFrame(this.call_render); + this.animation_frame_handle = requestAnimationFrame(this.call_render); }; } From 8e13441f2638717892847c4b68cec2d8a427e9ad Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Thu, 22 Aug 2019 22:45:01 +0200 Subject: [PATCH 13/50] Improved observables and ported more of the quest editor to the new GUI system. --- src/application/gui/ApplicationView.ts | 6 +- src/application/gui/MainContentView.ts | 13 +- src/application/gui/NavigationView.css | 5 +- src/application/gui/NavigationView.ts | 14 +- src/core/gui/Button.css | 15 +- src/core/gui/Button.ts | 15 +- src/core/gui/CheckBox.ts | 8 +- src/core/gui/FileButton.ts | 37 ++- src/core/gui/Label.ts | 6 +- src/core/gui/LazyView.ts | 4 +- src/core/gui/NumberInput.ts | 15 +- src/core/gui/RendererView.ts | 4 +- src/core/gui/TabContainer.css | 2 +- src/core/gui/TabContainer.ts | 15 +- src/core/gui/ToolBar.ts | 6 +- src/core/gui/View.ts | 20 +- src/core/gui/dom.ts | 40 +++- src/core/gui/golden_layout_theme.css | 52 +++++ .../observable/AbstractMinimalProperty.ts | 45 ++++ src/core/observable/AbstractProperty.ts | 14 ++ src/core/observable/ArrayProperty.ts | 7 + src/core/observable/DependentProperty.ts | 65 ++++++ src/core/observable/Disposer.ts | 4 + src/core/observable/Emitter.ts | 4 +- src/core/observable/FlatMappedProperty.ts | 70 ++++++ src/core/observable/MappedProperty.ts | 57 ----- src/core/observable/Observable.ts | 4 +- src/core/observable/Property.ts | 10 +- src/core/observable/SimpleEmitter.ts | 10 +- src/core/observable/SimpleProperty.ts | 41 ++-- .../observable/SimpleWritableArrayProperty.ts | 70 ++++++ src/core/observable/WritableArrayProperty.ts | 12 + src/core/observable/WritableProperty.ts | 15 +- src/core/observable/index.ts | 35 ++- src/core/stores/GuiStore.ts | 2 +- src/core/undo/Action.ts | 7 + src/core/undo/SimpleUndo.ts | 64 ++++++ src/core/undo/Undo.ts | 28 +++ src/core/undo/UndoManager.ts | 25 +++ src/core/undo/UndoStack.ts | 82 +++++++ src/core/undo/index.test.ts | 70 ++++++ src/core/undo/noop_undo.ts | 26 +++ src/quest_editor/domain/ObservableQuest.ts | 28 +++ .../domain/ObservableQuestEntity.ts | 9 + src/quest_editor/domain/ObservableQuestNpc.ts | 8 + .../domain/ObservableQuestObject.ts | 8 + src/quest_editor/gui/NpcCountsView.ts | 6 + src/quest_editor/gui/QuesInfoView.ts | 39 ++++ src/quest_editor/gui/QuestEditorView.ts | 211 +++++++++++++----- src/quest_editor/gui/ToolBarView.ts | 22 ++ .../persistence/QuestEditorUiPersister.ts | 8 +- src/quest_editor/stores/QuestEditorStore.ts | 122 ++++++++++ src/viewer/gui/ModelView.ts | 17 +- src/viewer/gui/TextureView.ts | 4 +- src/viewer/rendering/ModelRenderer.ts | 12 +- src/viewer/stores/ModelStore.ts | 32 +-- src/viewer/stores/TextureStore.ts | 2 +- 57 files changed, 1292 insertions(+), 280 deletions(-) create mode 100644 src/core/gui/golden_layout_theme.css create mode 100644 src/core/observable/AbstractMinimalProperty.ts create mode 100644 src/core/observable/AbstractProperty.ts create mode 100644 src/core/observable/ArrayProperty.ts create mode 100644 src/core/observable/DependentProperty.ts create mode 100644 src/core/observable/FlatMappedProperty.ts delete mode 100644 src/core/observable/MappedProperty.ts create mode 100644 src/core/observable/SimpleWritableArrayProperty.ts create mode 100644 src/core/observable/WritableArrayProperty.ts create mode 100644 src/core/undo/Action.ts create mode 100644 src/core/undo/SimpleUndo.ts create mode 100644 src/core/undo/Undo.ts create mode 100644 src/core/undo/UndoManager.ts create mode 100644 src/core/undo/UndoStack.ts create mode 100644 src/core/undo/index.test.ts create mode 100644 src/core/undo/noop_undo.ts create mode 100644 src/quest_editor/domain/ObservableQuest.ts create mode 100644 src/quest_editor/domain/ObservableQuestEntity.ts create mode 100644 src/quest_editor/domain/ObservableQuestNpc.ts create mode 100644 src/quest_editor/domain/ObservableQuestObject.ts create mode 100644 src/quest_editor/gui/NpcCountsView.ts create mode 100644 src/quest_editor/gui/QuesInfoView.ts create mode 100644 src/quest_editor/stores/QuestEditorStore.ts diff --git a/src/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts index 22b5f795..e9887564 100644 --- a/src/application/gui/ApplicationView.ts +++ b/src/application/gui/ApplicationView.ts @@ -1,10 +1,10 @@ import { NavigationView } from "./NavigationView"; import { MainContentView } from "./MainContentView"; -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; export class ApplicationView extends ResizableView { - element = create_el("div", "application_ApplicationView"); + element = el("div", { class: "application_ApplicationView" }); private menu_view = this.disposable(new NavigationView()); private main_content_view = this.disposable(new MainContentView()); @@ -12,6 +12,8 @@ export class ApplicationView extends ResizableView { constructor() { super(); + this.element.id = "root"; + this.element.append(this.menu_view.element, this.main_content_view.element); } diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 4e6cf3f3..4c46ca21 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,4 +1,4 @@ -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { LazyView } from "../../core/gui/LazyView"; import { ResizableView } from "../../core/gui/ResizableView"; @@ -12,7 +12,7 @@ const TOOLS: [GuiTool, () => Promise][] = [ ]; export class MainContentView extends ResizableView { - element = create_el("div", "application_MainContentView"); + element = el("div", { class: "application_MainContentView" }); private tool_views = new Map( TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]), @@ -25,7 +25,7 @@ export class MainContentView extends ResizableView { this.element.append(tool_view.element); } - const tool_view = this.tool_views.get(gui_store.tool.get()); + const tool_view = this.tool_views.get(gui_store.tool.val); if (tool_view) tool_view.visible = true; this.disposable(gui_store.tool.observe(this.tool_changed)); @@ -41,9 +41,10 @@ export class MainContentView extends ResizableView { return this; } - private tool_changed = (new_tool: GuiTool, { old_value }: { old_value: GuiTool }) => { - const old_view = this.tool_views.get(old_value); - if (old_view) old_view.visible = false; + private tool_changed = (new_tool: GuiTool) => { + for (const tool of this.tool_views.values()) { + tool.visible = false; + } const new_view = this.tool_views.get(new_tool); if (new_view) new_view.visible = true; diff --git a/src/application/gui/NavigationView.css b/src/application/gui/NavigationView.css index cb8cf677..75932b7c 100644 --- a/src/application/gui/NavigationView.css +++ b/src/application/gui/NavigationView.css @@ -13,11 +13,12 @@ .application_ToolButton label { box-sizing: border-box; - display: inline-block; + display: inline-flex; + flex-direction: row; + align-items: center; font-size: 15px; height: 100%; padding: 0 20px; - line-height: 29px; color: hsl(0, 0%, 65%); } diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 5dfe0e45..5826018b 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -1,4 +1,4 @@ -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import "./NavigationView.css"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { View } from "../../core/gui/View"; @@ -10,7 +10,7 @@ const TOOLS: [GuiTool, string][] = [ ]; export class NavigationView extends View { - readonly element = create_el("div", "application_NavigationView"); + readonly element = el("div", { class: "application_NavigationView" }); readonly height = 30; @@ -28,13 +28,13 @@ export class NavigationView extends View { this.element.append(button.element); } - this.tool_changed(gui_store.tool.get()); + this.tool_changed(gui_store.tool.val); this.disposable(gui_store.tool.observe(this.tool_changed)); } private click(e: MouseEvent): void { if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { - gui_store.tool.set((GuiTool as any)[e.target.control.value]); + gui_store.tool.val = (GuiTool as any)[e.target.control.value]; } } @@ -45,10 +45,10 @@ export class NavigationView extends View { } class ToolButton extends View { - element: HTMLElement = create_el("span"); + element: HTMLElement = el("span"); - private input: HTMLInputElement = create_el("input"); - private label: HTMLLabelElement = create_el("label"); + private input: HTMLInputElement = el("input"); + private label: HTMLLabelElement = el("label"); constructor(tool: GuiTool, text: string) { super(); diff --git a/src/core/gui/Button.css b/src/core/gui/Button.css index 4e7b50d0..9d05f70a 100644 --- a/src/core/gui/Button.css +++ b/src/core/gui/Button.css @@ -1,5 +1,7 @@ .core_Button { - display: inline-block; + display: inline-flex; + flex-direction: row; + align-items: stretch; box-sizing: border-box; padding: 0; border: solid 1px hsl(0, 0%, 10%); @@ -8,11 +10,12 @@ } .core_Button .core_Button_inner { - display: inline-block; + display: flex; + flex-direction: row; + align-items: center; box-sizing: border-box; background-color: hsl(0, 0%, 20%); height: 24px; - line-height: 17px; padding: 3px 8px; border: solid 1px hsl(0, 0%, 35%); } @@ -28,3 +31,9 @@ border-color: hsl(0, 0%, 30%); color: hsl(0, 0%, 75%); } + +.core_Button:disabled .core_Button_inner { + background-color: hsl(0, 0%, 15%); + border-color: hsl(0, 0%, 25%); + color: hsl(0, 0%, 55%); +} diff --git a/src/core/gui/Button.ts b/src/core/gui/Button.ts index aa0c0856..7c7a03c7 100644 --- a/src/core/gui/Button.ts +++ b/src/core/gui/Button.ts @@ -1,11 +1,11 @@ -import { create_el } from "./dom"; -import { View } from "./View"; +import { el } from "./dom"; import "./Button.css"; import { Observable } from "../observable/Observable"; import { emitter } from "../observable"; +import { Control } from "./Control"; -export class Button extends View { - readonly element: HTMLButtonElement = create_el("button", "core_Button"); +export class Button extends Control { + readonly element: HTMLButtonElement = el("button", { class: "core_Button" }); private readonly _click = emitter(); readonly click: Observable = this._click; @@ -13,11 +13,10 @@ export class Button extends View { constructor(text: string) { super(); - const inner_element = create_el("span", "core_Button_inner"); - inner_element.textContent = text; + this.element.append(el("span", { class: "core_Button_inner", text })); - this.element.append(inner_element); + this.enabled.observe(enabled => (this.element.disabled = !enabled)); - this.element.onclick = (e: MouseEvent) => this._click.emit(e, undefined); + this.element.onclick = (e: MouseEvent) => this._click.emit(e); } } diff --git a/src/core/gui/CheckBox.ts b/src/core/gui/CheckBox.ts index 3e64461f..ca6dcf2e 100644 --- a/src/core/gui/CheckBox.ts +++ b/src/core/gui/CheckBox.ts @@ -1,10 +1,10 @@ -import { create_el } from "./dom"; +import { el } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import { property } from "../observable"; import { LabelledControl } from "./LabelledControl"; export class CheckBox extends LabelledControl { - readonly element: HTMLInputElement = create_el("input", "core_CheckBox"); + readonly element: HTMLInputElement = el("input", { class: "core_CheckBox" }); readonly checked: WritableProperty = property(false); @@ -14,7 +14,7 @@ export class CheckBox extends LabelledControl { super(label); this.element.type = "checkbox"; - this.element.onchange = () => this.checked.set(this.element.checked); + this.element.onchange = () => (this.checked.val = this.element.checked); this.disposables( this.checked.observe(checked => (this.element.checked = checked)), @@ -22,6 +22,6 @@ export class CheckBox extends LabelledControl { this.enabled.observe(enabled => (this.element.disabled = !enabled)), ); - this.checked.set(checked); + this.checked.val = checked; } } diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index 0b20c0d2..e3227f7c 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -1,17 +1,21 @@ -import { create_el } from "./dom"; -import { View } from "./View"; +import { el } from "./dom"; import "./FileButton.css"; import "./Button.css"; import { property } from "../observable"; import { Property } from "../observable/Property"; +import { Control } from "./Control"; -export class FileButton extends View { - readonly element: HTMLLabelElement = create_el("label", "core_FileButton core_Button"); +export class FileButton extends Control { + readonly element: HTMLLabelElement = el("label", { + class: "core_FileButton core_Button", + }); private readonly _files = property([]); readonly files: Property = this._files; - private input: HTMLInputElement = create_el("input", "core_FileButton_input"); + private input: HTMLInputElement = el("input", { + class: "core_FileButton_input core_Button_inner", + }); constructor(text: string, accept: string = "") { super(); @@ -20,15 +24,28 @@ export class FileButton extends View { this.input.accept = accept; this.input.onchange = () => { if (this.input.files && this.input.files.length) { - this._files.set([...this.input.files!]); + this._files.val = [...this.input.files!]; } else { - this._files.set([]); + this._files.val = []; } }; - const inner_element = create_el("span", "core_FileButton_inner core_Button_inner"); - inner_element.textContent = text; + this.element.append( + el("span", { + class: "core_FileButton_inner core_Button_inner", + text, + }), + this.input, + ); - this.element.append(inner_element, this.input); + this.enabled.observe(enabled => { + this.input.disabled = !enabled; + + if (enabled) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } + }); } } diff --git a/src/core/gui/Label.ts b/src/core/gui/Label.ts index 919ef8cb..785cf6d9 100644 --- a/src/core/gui/Label.ts +++ b/src/core/gui/Label.ts @@ -1,12 +1,12 @@ import { View } from "./View"; -import { create_el } from "./dom"; +import { el } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import "./Label.css"; import { property } from "../observable"; import { Property } from "../observable/Property"; export class Label extends View { - readonly element = create_el("label", "core_Label"); + readonly element = el("label", { class: "core_Label" }); set for(id: string) { this.element.htmlFor = id; @@ -20,7 +20,7 @@ export class Label extends View { if (typeof text === "string") { this.element.append(text); } else { - this.element.append(text.get()); + this.element.append(text.val); this.disposable(text.observe(text => (this.element.textContent = text))); } diff --git a/src/core/gui/LazyView.ts b/src/core/gui/LazyView.ts index e05d43cf..24f81132 100644 --- a/src/core/gui/LazyView.ts +++ b/src/core/gui/LazyView.ts @@ -1,10 +1,10 @@ import { View } from "./View"; -import { create_el } from "./dom"; +import { el } from "./dom"; import { Resizable } from "./Resizable"; import { ResizableView } from "./ResizableView"; export class LazyView extends ResizableView { - readonly element = create_el("div", "core_LazyView"); + readonly element = el("div", { class: "core_LazyView" }); private _visible = false; diff --git a/src/core/gui/NumberInput.ts b/src/core/gui/NumberInput.ts index 9700cb02..e6b37bf8 100644 --- a/src/core/gui/NumberInput.ts +++ b/src/core/gui/NumberInput.ts @@ -1,22 +1,21 @@ import "./NumberInput.css"; import "./Input.css"; -import { create_el } from "./dom"; +import { el } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import { property } from "../observable"; import { LabelledControl } from "./LabelledControl"; import { is_any_property, Property } from "../observable/Property"; export class NumberInput extends LabelledControl { - readonly element = create_el("span", "core_NumberInput core_Input"); + readonly element = el("span", { class: "core_NumberInput core_Input" }); readonly value: WritableProperty = property(0); readonly preferred_label_position = "left"; - private readonly input: HTMLInputElement = create_el( - "input", - "core_NumberInput_inner core_Input_inner", - ); + private readonly input: HTMLInputElement = el("input", { + class: "core_NumberInput_inner core_Input_inner", + }); constructor( value = 0, @@ -34,7 +33,7 @@ export class NumberInput extends LabelledControl { this.set_prop("max", max); this.set_prop("step", step); - this.input.onchange = () => this.value.set(this.input.valueAsNumber); + this.input.onchange = () => (this.value.val = this.input.valueAsNumber); this.element.append(this.input); @@ -57,7 +56,7 @@ export class NumberInput extends LabelledControl { private set_prop(prop: "min" | "max" | "step", value: T | Property): void { if (is_any_property(value)) { - this.input[prop] = String(value.get()); + this.input[prop] = String(value.val); this.disposable(value.observe(v => (this.input[prop] = String(v)))); } else { this.input[prop] = String(value); diff --git a/src/core/gui/RendererView.ts b/src/core/gui/RendererView.ts index 2756fb00..87d08175 100644 --- a/src/core/gui/RendererView.ts +++ b/src/core/gui/RendererView.ts @@ -1,9 +1,9 @@ import { ResizableView } from "./ResizableView"; -import { create_el } from "./dom"; +import { el } from "./dom"; import { Renderer } from "../rendering/Renderer"; export class RendererView extends ResizableView { - readonly element = create_el("div"); + readonly element = el("div"); constructor(private renderer: Renderer) { super(); diff --git a/src/core/gui/TabContainer.css b/src/core/gui/TabContainer.css index 68c14863..9ed91b04 100644 --- a/src/core/gui/TabContainer.css +++ b/src/core/gui/TabContainer.css @@ -1,6 +1,6 @@ .core_TabContainer_Bar { box-sizing: border-box; - padding: 3px 0 0 0; + padding: 3px 3px 0 3px; border-bottom: solid 1px var(--border-color); } diff --git a/src/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts index 3d017d2c..1add86fc 100644 --- a/src/core/gui/TabContainer.ts +++ b/src/core/gui/TabContainer.ts @@ -1,5 +1,5 @@ import { View } from "./View"; -import { create_el } from "./dom"; +import { el } from "./dom"; import { LazyView } from "./LazyView"; import { Resizable } from "./Resizable"; import { ResizableView } from "./ResizableView"; @@ -16,11 +16,11 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; const BAR_HEIGHT = 28; export class TabContainer extends ResizableView { - readonly element = create_el("div", "core_TabContainer"); + readonly element = el("div", { class: "core_TabContainer" }); private tabs: TabInfo[] = []; - private bar_element = create_el("div", "core_TabContainer_Bar"); - private panes_element = create_el("div", "core_TabContainer_Panes"); + private bar_element = el("div", { class: "core_TabContainer_Bar" }); + private panes_element = el("div", { class: "core_TabContainer_Panes" }); constructor(...tabs: Tab[]) { super(); @@ -28,9 +28,10 @@ export class TabContainer extends ResizableView { 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; + const tab_element = el("span", { + class: "core_TabContainer_Tab", + text: tab.title, + data: { key: tab.key }, }); this.bar_element.append(tab_element); diff --git a/src/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index 009d41d4..1c0aeb41 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -1,10 +1,10 @@ import { View } from "./View"; -import { create_el } from "./dom"; +import { el } from "./dom"; import "./ToolBar.css"; import { LabelledControl } from "./LabelledControl"; export class ToolBar extends View { - readonly element = create_el("div", "core_ToolBar"); + readonly element = el("div", { class: "core_ToolBar" }); readonly height = 33; constructor(...children: View[]) { @@ -14,7 +14,7 @@ export class ToolBar extends View { for (const child of children) { if (child instanceof LabelledControl) { - const group = create_el("div", "core_ToolBar_group"); + const group = el("div", { class: "core_ToolBar_group" }); if (child.preferred_label_position === "left") { group.append(child.label.element, child.element); diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts index c9101580..3bc31f53 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/View.ts @@ -1,5 +1,7 @@ import { Disposable } from "../observable/Disposable"; import { Disposer } from "../observable/Disposer"; +import { Observable } from "../observable/Observable"; +import { bind_hidden } from "./dom"; export abstract class View implements Disposable { abstract readonly element: HTMLElement; @@ -14,6 +16,19 @@ export abstract class View implements Disposable { private disposer = new Disposer(); + dispose(): void { + this.element.remove(); + this.disposer.dispose(); + } + + protected bind_hidden(element: HTMLElement, observable: Observable): void { + this.disposable(bind_hidden(element, observable)); + } + + protected bind_disabled(element: HTMLElement, observable: Observable): void { + this.disposable(bind_hidden(element, observable)); + } + protected disposable(disposable: T): T { return this.disposer.add(disposable); } @@ -21,9 +36,4 @@ export abstract class View implements Disposable { protected disposables(...disposables: Disposable[]): void { this.disposer.add_all(...disposables); } - - dispose(): void { - this.element.remove(); - this.disposer.dispose(); - } } diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index ea4b0d46..3bb42a36 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -1,20 +1,38 @@ import { Disposable } from "../observable/Disposable"; +import { Observable } from "../observable/Observable"; +import { is_property } from "../observable/Property"; -export function create_el( +export function el( tag_name: string, - class_name?: string, - modify?: (element: T) => void, + attributes?: { + class?: string; + text?: string ; + data?: { [key: string]: string }; + }, + ...children: HTMLElement[] ): T { const element = document.createElement(tag_name) as T; - if (class_name) element.className = class_name; - if (modify) modify(element); + + if (attributes) { + if (attributes.class) element.className = attributes.class; + if (attributes.text) element.textContent = attributes.text; + + if (attributes.data) { + for (const [key, val] of Object.entries(attributes.data)) { + element.dataset[key] = val; + } + } + } + + element.append(...children); + return element; } -export function disposable_el(element: HTMLElement): Disposable { - return { - dispose(): void { - element.remove(); - }, - }; +export function bind_hidden(element: HTMLElement, observable: Observable): Disposable { + if (is_property(observable)) { + element.hidden = observable.val; + } + + return observable.observe(v => (element.hidden = v)); } diff --git a/src/core/gui/golden_layout_theme.css b/src/core/gui/golden_layout_theme.css new file mode 100644 index 00000000..9a23367c --- /dev/null +++ b/src/core/gui/golden_layout_theme.css @@ -0,0 +1,52 @@ +#root .lm_header { + box-sizing: border-box; + padding: 3px 0 0 0; + border-bottom: solid 1px var(--border-color); +} + +#root .lm_tabs { + padding: 0 3px; +} + +#root .lm_tab { + cursor: default; + height: 21px; + line-height: 22px; + padding: 0 10px; + border: solid 1px var(--border-color); + margin: 0 1px -1px 1px; + background-color: hsl(0, 0%, 12%); + color: hsl(0, 0%, 75%); + font-size: 15px; +} + +#root .lm_tab:hover { + background-color: hsl(0, 0%, 18%); + color: hsl(0, 0%, 85%); +} + +#root .lm_tab.lm_active { + background-color: var(--bg-color); + color: hsl(0, 0%, 90%); + border-bottom-color: var(--bg-color); +} + +#root .lm_splitter { + box-sizing: border-box; + background-color: hsl(0, 0%, 20%); +} + +#root .lm_splitter.lm_vertical { + border-top: solid 1px var(--border-color); + border-bottom: solid 1px var(--border-color); +} + +#root .lm_splitter.lm_horizontal { + border-left: solid 1px var(--border-color); + border-right: solid 1px var(--border-color); +} + +body .lm_dropTargetIndicator { + box-sizing: border-box; + background-color: hsla(0, 0%, 100%, 0.2); +} diff --git a/src/core/observable/AbstractMinimalProperty.ts b/src/core/observable/AbstractMinimalProperty.ts new file mode 100644 index 00000000..9925ce80 --- /dev/null +++ b/src/core/observable/AbstractMinimalProperty.ts @@ -0,0 +1,45 @@ +import { Property } from "./Property"; +import { Disposable } from "./Disposable"; +import Logger from "js-logger"; + +const logger = Logger.get("core/observable/AbstractMinimalProperty"); + +// This class exists purely because otherwise the resulting cyclic dependency graph would trip up commonjs. +// The dependency graph is still cyclic but for some reason it's not a problem this way. +export abstract class AbstractMinimalProperty implements Property { + readonly is_property = true; + + abstract readonly val: T; + + protected readonly observers: ((value: T) => void)[] = []; + + observe(observer: (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); + } + }, + }; + } + + abstract map(f: (element: T) => U): Property; + + abstract flat_map(f: (element: T) => Property): Property; + + protected emit(): void { + for (const observer of this.observers) { + try { + observer(this.val); + } catch (e) { + logger.error("Observer threw error.", e); + } + } + } +} diff --git a/src/core/observable/AbstractProperty.ts b/src/core/observable/AbstractProperty.ts new file mode 100644 index 00000000..975f4dbb --- /dev/null +++ b/src/core/observable/AbstractProperty.ts @@ -0,0 +1,14 @@ +import { Property } from "./Property"; +import { DependentProperty } from "./DependentProperty"; +import { FlatMappedProperty } from "./FlatMappedProperty"; +import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; + +export abstract class AbstractProperty extends AbstractMinimalProperty { + map(f: (element: T) => U): Property { + return new DependentProperty([this], () => f(this.val)); + } + + flat_map(f: (element: T) => Property): Property { + return new FlatMappedProperty(this, value => f(value)); + } +} diff --git a/src/core/observable/ArrayProperty.ts b/src/core/observable/ArrayProperty.ts new file mode 100644 index 00000000..3da6adce --- /dev/null +++ b/src/core/observable/ArrayProperty.ts @@ -0,0 +1,7 @@ +import { Property } from "./Property"; + +export interface ArrayProperty extends Property { + get(index: number): T; + + readonly length: Property; +} diff --git a/src/core/observable/DependentProperty.ts b/src/core/observable/DependentProperty.ts new file mode 100644 index 00000000..494c494c --- /dev/null +++ b/src/core/observable/DependentProperty.ts @@ -0,0 +1,65 @@ +import { Disposable } from "./Disposable"; +import { Property } from "./Property"; +import { Disposer } from "./Disposer"; +import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; +import { FlatMappedProperty } from "./FlatMappedProperty"; + +/** + * Starts observing its dependencies when the first observer on this property is registered. + * Stops observing its dependencies when the last observer on this property is disposed. + * This way no extra disposables need to be managed when e.g. {@link Property.map} is used. + */ +export class DependentProperty extends AbstractMinimalProperty implements Property { + readonly is_property = true; + + private _val?: T; + + get val(): T { + if (this.dependency_disposables) { + return this._val as T; + } else { + return this.f(); + } + } + + private dependency_disposables = new Disposer(); + + constructor(private dependencies: Property[], private f: () => T) { + super(); + } + + observe(observer: (event: T) => void): Disposable { + const super_disposable = super.observe(observer); + + if (this.dependency_disposables.length === 0) { + this._val = this.f(); + + this.dependency_disposables.add_all( + ...this.dependencies.map(dependency => + dependency.observe(() => { + this._val = this.f(); + this.emit(); + }), + ), + ); + } + + return { + dispose: () => { + super_disposable.dispose(); + + if (this.observers.length === 0) { + this.dependency_disposables.dispose(); + } + }, + }; + } + + map(f: (element: T) => U): Property { + return new DependentProperty([this], () => f(this.val)); + } + + flat_map(f: (element: T) => Property): Property { + return new FlatMappedProperty(this, value => f(value)); + } +} diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index fd15e14a..5ae1ae1d 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -6,6 +6,10 @@ const logger = Logger.get("core/observable/Disposer"); export class Disposer implements Disposable { private readonly disposables: Disposable[] = []; + get length(): number { + return this.disposables.length; + } + add(disposable: T): T { this.disposables.push(disposable); return disposable; diff --git a/src/core/observable/Emitter.ts b/src/core/observable/Emitter.ts index 54a3a53b..6306e3f6 100644 --- a/src/core/observable/Emitter.ts +++ b/src/core/observable/Emitter.ts @@ -1,5 +1,5 @@ import { Observable } from "./Observable"; -export interface Emitter extends Observable { - emit(event: E, meta: M): void; +export interface Emitter extends Observable { + emit(event: E): void; } diff --git a/src/core/observable/FlatMappedProperty.ts b/src/core/observable/FlatMappedProperty.ts new file mode 100644 index 00000000..61a30c39 --- /dev/null +++ b/src/core/observable/FlatMappedProperty.ts @@ -0,0 +1,70 @@ +import { Property } from "./Property"; +import { Disposable } from "./Disposable"; +import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; +import { DependentProperty } from "./DependentProperty"; + +/** + * Starts observing its dependency when the first observer on this property is registered. + * Stops observing its dependency when the last observer on this property is disposed. + * This way no extra disposables need to be managed when {@link Property.flat_map} is used. + */ +export class FlatMappedProperty extends AbstractMinimalProperty implements Property { + readonly is_property = true; + + get val(): U { + return this.computed_property + ? this.computed_property.val + : this.f(this.dependency.val).val; + } + + private dependency_disposable?: Disposable; + private computed_property?: Property; + private computed_disposable?: Disposable; + + constructor(private dependency: Property, private f: (value: T) => Property) { + super(); + } + + observe(observer: (value: U) => void): Disposable { + const super_disposable = super.observe(observer); + + if (this.dependency_disposable == undefined) { + this.dependency_disposable = this.dependency.observe(() => { + this.compute_and_observe(); + this.emit(); + }); + + this.compute_and_observe(); + } + + return { + dispose: () => { + super_disposable.dispose(); + + if (this.observers.length === 0) { + this.dependency_disposable!.dispose(); + this.dependency_disposable = undefined; + this.computed_disposable!.dispose(); + this.computed_disposable = undefined; + this.computed_property = undefined; + } + }, + }; + } + + map(f: (element: U) => V): Property { + return new DependentProperty([this], () => f(this.val)); + } + + flat_map(f: (element: U) => Property): Property { + return new FlatMappedProperty(this, value => f(value)); + } + + private compute_and_observe(): void { + if (this.computed_disposable) this.computed_disposable.dispose(); + this.computed_property = this.f(this.dependency.val); + this.computed_disposable = this.computed_property.observe(() => { + this.emit(); + }); + } +} diff --git a/src/core/observable/MappedProperty.ts b/src/core/observable/MappedProperty.ts deleted file mode 100644 index 238b4dde..00000000 --- a/src/core/observable/MappedProperty.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { SimpleEmitter } from "./SimpleEmitter"; -import { Disposable } from "./Disposable"; -import { Property, PropertyMeta } from "./Property"; - -/** - * Starts observing its origin when the first observer on this property is registered. - * Stops observing its origin when the last observer on this property is disposed. - * This way no extra disposables need to be managed when {@link Property.map} is used. - */ -export class MappedProperty extends SimpleEmitter> implements Property { - readonly is_property = true; - - private origin_disposable?: Disposable; - private value?: T; - - constructor(private origin: Property, private f: (value: S) => T) { - super(); - } - - observe(observer: (event: T, meta: PropertyMeta) => void): Disposable { - const disposable = super.observe(observer); - - if (this.origin_disposable == undefined) { - this.value = this.f(this.origin.get()); - - this.origin_disposable = this.origin.observe(origin_value => { - const old_value = this.value as T; - this.value = this.f(origin_value); - - this.emit(this.value, { old_value }); - }); - } - - return { - dispose: () => { - disposable.dispose(); - - if (this.observers.length === 0) { - this.origin_disposable!.dispose(); - this.origin_disposable = undefined; - } - }, - }; - } - - get(): T { - if (this.origin_disposable) { - return this.value as T; - } else { - return this.f(this.origin.get()); - } - } - - map(f: (element: T) => U): Property { - return new MappedProperty(this, f); - } -} diff --git a/src/core/observable/Observable.ts b/src/core/observable/Observable.ts index dffd4e3e..9b89004a 100644 --- a/src/core/observable/Observable.ts +++ b/src/core/observable/Observable.ts @@ -1,5 +1,5 @@ import { Disposable } from "./Disposable"; -export interface Observable { - observe(observer: (event: E, meta: M) => void): Disposable; +export interface Observable { + observe(observer: (event: E) => void): Disposable; } diff --git a/src/core/observable/Property.ts b/src/core/observable/Property.ts index 70ef2889..9efbc1a7 100644 --- a/src/core/observable/Property.ts +++ b/src/core/observable/Property.ts @@ -1,16 +1,16 @@ import { Observable } from "./Observable"; -export interface Property extends Observable> { +export interface Property extends Observable { readonly is_property: true; - get(): T; + readonly val: T; map(f: (element: T) => U): Property; + + flat_map(f: (element: T) => Property): Property; } -export type PropertyMeta = { old_value: T }; - -export function is_property(observable: Observable): observable is Property { +export function is_property(observable: Observable): observable is Property { return (observable as any).is_property; } diff --git a/src/core/observable/SimpleEmitter.ts b/src/core/observable/SimpleEmitter.ts index 3c303dad..2c584447 100644 --- a/src/core/observable/SimpleEmitter.ts +++ b/src/core/observable/SimpleEmitter.ts @@ -3,20 +3,20 @@ import Logger from "js-logger"; const logger = Logger.get("core/observable/SimpleEmitter"); -export class SimpleEmitter { - protected readonly observers: ((event: E, meta: M) => void)[] = []; +export class SimpleEmitter { + protected readonly observers: ((event: E) => void)[] = []; - emit(event: E, meta: M): void { + emit(event: E): void { for (const observer of this.observers) { try { - observer(event, meta); + observer(event); } catch (e) { logger.error("Observer threw error.", e); } } } - observe(observer: (event: E, meta: M) => void): Disposable { + observe(observer: (event: E) => void): Disposable { if (!this.observers.includes(observer)) { this.observers.push(observer); } diff --git a/src/core/observable/SimpleProperty.ts b/src/core/observable/SimpleProperty.ts index ce8ddbd9..d60bf279 100644 --- a/src/core/observable/SimpleProperty.ts +++ b/src/core/observable/SimpleProperty.ts @@ -1,40 +1,37 @@ -import { SimpleEmitter } from "./SimpleEmitter"; import { Disposable } from "./Disposable"; import { Observable } from "./Observable"; import { WritableProperty } from "./WritableProperty"; -import { Property, PropertyMeta, is_property } from "./Property"; -import { MappedProperty } from "./MappedProperty"; +import { is_property } from "./Property"; +import { AbstractProperty } from "./AbstractProperty"; -export class SimpleProperty extends SimpleEmitter> - implements WritableProperty { - readonly is_property = true; +export class SimpleProperty extends AbstractProperty implements WritableProperty { readonly is_writable_property = true; - private value: T; - - constructor(value: T) { + constructor(private _val: T) { super(); - this.value = value; } - get(): T { - return this.value; + get val(): T { + return this._val; } - set(value: T): void { - if (value !== this.value) { - const old_value = this.value; - this.value = value; - this.emit(value, { old_value }); + set val(val: T) { + if (val !== this._val) { + this._val = val; + this.emit(); } } - bind(observable: Observable): Disposable { + update(f: (value: T) => T): void { + this.val = f(this.val); + } + + bind(observable: Observable): Disposable { if (is_property(observable)) { - this.set(observable.get()); + this.val = observable.val; } - return observable.observe(v => this.set(v)); + return observable.observe(v => (this.val = v)); } bind_bi(property: WritableProperty): Disposable { @@ -47,8 +44,4 @@ export class SimpleProperty extends SimpleEmitter> }, }; } - - map(f: (element: T) => U): Property { - return new MappedProperty(this, f); - } } diff --git a/src/core/observable/SimpleWritableArrayProperty.ts b/src/core/observable/SimpleWritableArrayProperty.ts new file mode 100644 index 00000000..27dd9450 --- /dev/null +++ b/src/core/observable/SimpleWritableArrayProperty.ts @@ -0,0 +1,70 @@ +/* eslint-disable no-dupe-class-members */ +import { WritableArrayProperty } from "./WritableArrayProperty"; +import { Disposable } from "./Disposable"; +import { WritableProperty } from "./WritableProperty"; +import { Observable } from "./Observable"; +import { property } from "./index"; +import { AbstractProperty } from "./AbstractProperty"; + +export class SimpleWritableArrayProperty extends AbstractProperty + implements WritableArrayProperty { + readonly is_property = true; + + readonly is_writable_property = true; + + private readonly _length = property(0); + readonly length = this._length; + + private readonly values: T[]; + + get val(): T[] { + return this.values; + } + + constructor(...values: T[]) { + super(); + this.values = values; + } + + bind(observable: Observable): Disposable { + /* TODO */ throw new Error("not implemented"); + } + + bind_bi(property: WritableProperty): Disposable { + /* TODO */ throw new Error("not implemented"); + } + + update(f: (value: T[]) => T[]): void { + this.values.splice(0, this.values.length, ...f(this.values)); + } + + get(index: number): T { + return this.values[index]; + } + + set(index: number, value: T): void { + this.values[index] = value; + this.emit(); + } + + clear(): void { + this.values.splice(0, this.values.length); + this.emit(); + } + + splice(index: number, delete_count?: number): T[]; + splice(index: number, delete_count: number, ...items: T[]): T[]; + splice(index: number, delete_count?: number, ...items: T[]): T[] { + let ret: T[]; + + if (delete_count == undefined) { + ret = this.values.splice(index); + } else { + ret = this.values.splice(index, delete_count, ...items); + } + + this.emit(); + + return ret; + } +} diff --git a/src/core/observable/WritableArrayProperty.ts b/src/core/observable/WritableArrayProperty.ts new file mode 100644 index 00000000..e09847f5 --- /dev/null +++ b/src/core/observable/WritableArrayProperty.ts @@ -0,0 +1,12 @@ +import { WritableProperty } from "./WritableProperty"; +import { ArrayProperty } from "./ArrayProperty"; + +export interface WritableArrayProperty extends ArrayProperty, WritableProperty { + val: T[]; + + set(index: number, value: T): void; + + splice(index: number, delete_count?: number, ...items: T[]): T[]; + + clear(): void; +} diff --git a/src/core/observable/WritableProperty.ts b/src/core/observable/WritableProperty.ts index 64f464b2..a8d9a4b5 100644 --- a/src/core/observable/WritableProperty.ts +++ b/src/core/observable/WritableProperty.ts @@ -3,17 +3,24 @@ import { Observable } from "./Observable"; import { Disposable } from "./Disposable"; export interface WritableProperty extends Property { - is_writable_property: true; + readonly is_writable_property: true; - set(value: T): void; + val: T; - bind(observable: Observable): Disposable; + update(f: (value: T) => T): void; + + /** + * Bind the value of this property to the given observable. + * + * @param observable the observable who's events will be propagated to this property. + */ + bind(observable: Observable): Disposable; bind_bi(property: WritableProperty): Disposable; } export function is_writable_property( - observable: Observable, + observable: Observable, ): observable is WritableProperty { return (observable as any).is_writable_property; } diff --git a/src/core/observable/index.ts b/src/core/observable/index.ts index 864fa6ca..f34682e9 100644 --- a/src/core/observable/index.ts +++ b/src/core/observable/index.ts @@ -2,11 +2,44 @@ import { SimpleEmitter } from "./SimpleEmitter"; import { WritableProperty } from "./WritableProperty"; import { SimpleProperty } from "./SimpleProperty"; import { Emitter } from "./Emitter"; +import { Property } from "./Property"; +import { DependentProperty } from "./DependentProperty"; +import { WritableArrayProperty } from "./WritableArrayProperty"; +import { SimpleWritableArrayProperty } from "./SimpleWritableArrayProperty"; -export function emitter(): Emitter { +export function emitter(): Emitter { return new SimpleEmitter(); } export function property(value: T): WritableProperty { return new SimpleProperty(value); } + +export function array_property(...values: T[]): WritableArrayProperty { + return new SimpleWritableArrayProperty(...values); +} + +export function if_defined( + property: Property, + f: (value: S) => T, + default_value: T, +): T { + const val = property.val; + return val == undefined ? default_value : f(val); +} + +export function add(left: Property, right: number): Property { + return left.map(l => l + right); +} + +export function sub(left: Property, right: number): Property { + return left.map(l => l - right); +} + +export function map( + f: (prop_1: S, prop_2: T) => R, + prop_1: Property, + prop_2: Property, +): Property { + return new DependentProperty([prop_1, prop_2], () => f(prop_1.val, prop_2.val)); +} diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index 02aa892a..3397ea51 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -24,7 +24,7 @@ class GuiStore implements Disposable { constructor() { const tool = window.location.hash.slice(2); - this.tool.set(string_to_gui_tool(tool) || GuiTool.Viewer); + this.tool.val = string_to_gui_tool(tool) || GuiTool.Viewer; } dispose(): void { diff --git a/src/core/undo/Action.ts b/src/core/undo/Action.ts new file mode 100644 index 00000000..910ab9a5 --- /dev/null +++ b/src/core/undo/Action.ts @@ -0,0 +1,7 @@ +export class Action { + constructor( + readonly description: string, + readonly undo: () => void, + readonly redo: () => void, + ) {} +} diff --git a/src/core/undo/SimpleUndo.ts b/src/core/undo/SimpleUndo.ts new file mode 100644 index 00000000..7cca7dd5 --- /dev/null +++ b/src/core/undo/SimpleUndo.ts @@ -0,0 +1,64 @@ +import { Undo } from "./Undo"; +import { Action } from "./Action"; +import { Property } from "../observable/Property"; +import { property } from "../observable"; +import { NOOP_UNDO } from "./noop_undo"; +import { undo_manager } from "./UndoManager"; + +/** + * Simply contains a single action. `can_undo` and `can_redo` must be managed manually. + */ +export class SimpleUndo implements Undo { + private readonly _action: Action; + readonly action: Property; + + constructor(description: string, undo: () => void, redo: () => void) { + this._action = new Action(description, undo, redo); + this.action = property(this._action); + } + + make_current(): void { + undo_manager.current.val = this; + } + + ensure_not_current(): void { + if (undo_manager.current.val === this) { + undo_manager.current.val = NOOP_UNDO; + } + } + + readonly can_undo = property(false); + + readonly can_redo = property(false); + + readonly first_undo: Property = this.can_undo.map(can_undo => + can_undo ? this._action : undefined, + ); + + readonly first_redo: Property = this.can_redo.map(can_redo => + can_redo ? this._action : undefined, + ); + + undo(): boolean { + if (this.can_undo) { + this._action.undo(); + return true; + } else { + return false; + } + } + + redo(): boolean { + if (this.can_redo) { + this._action.redo(); + return true; + } else { + return false; + } + } + + reset(): void { + this.can_undo.val = false; + this.can_redo.val = false; + } +} diff --git a/src/core/undo/Undo.ts b/src/core/undo/Undo.ts new file mode 100644 index 00000000..a6a82b8f --- /dev/null +++ b/src/core/undo/Undo.ts @@ -0,0 +1,28 @@ +import { Property } from "../observable/Property"; +import { Action } from "./Action"; + +export interface Undo { + make_current(): void; + + ensure_not_current(): void; + + readonly can_undo: Property; + + readonly can_redo: Property; + + /** + * The first action that will be undone when calling undo(). + */ + readonly first_undo: Property; + + /** + * The first action that will be redone when calling redo(). + */ + readonly first_redo: Property; + + undo(): boolean; + + redo(): boolean; + + reset(): void; +} diff --git a/src/core/undo/UndoManager.ts b/src/core/undo/UndoManager.ts new file mode 100644 index 00000000..fbe64b41 --- /dev/null +++ b/src/core/undo/UndoManager.ts @@ -0,0 +1,25 @@ +import { if_defined, property } from "../observable"; +import { Undo } from "./Undo"; +import { NOOP_UNDO } from "./noop_undo"; + +class UndoManager { + readonly current = property(NOOP_UNDO); + + can_undo = this.current.flat_map(c => c.can_undo); + + can_redo = this.current.flat_map(c => c.can_redo); + + first_undo = this.current.flat_map(c => c.first_undo); + + first_redo = this.current.flat_map(c => c.first_redo); + + undo(): boolean { + return if_defined(this.current, c => c.undo(), false); + } + + redo(): boolean { + return if_defined(this.current, c => c.redo(), false); + } +} + +export const undo_manager = new UndoManager(); diff --git a/src/core/undo/UndoStack.ts b/src/core/undo/UndoStack.ts new file mode 100644 index 00000000..22c8927a --- /dev/null +++ b/src/core/undo/UndoStack.ts @@ -0,0 +1,82 @@ +import { Undo } from "./Undo"; +import { WritableArrayProperty } from "../observable/WritableArrayProperty"; +import { Action } from "./Action"; +import { array_property, map, property } from "../observable"; +import { NOOP_UNDO } from "./noop_undo"; +import { undo_manager } from "./UndoManager"; + +/** + * Full-fledged linear undo/redo implementation. + */ +export class UndoStack implements Undo { + private readonly stack: WritableArrayProperty = array_property(); + + /** + * The index where new actions are inserted. + */ + private readonly index = property(0); + + make_current(): void { + undo_manager.current.val = this; + } + + ensure_not_current(): void { + if (undo_manager.current.val === this) { + undo_manager.current.val = NOOP_UNDO; + } + } + + readonly can_undo = this.index.map(index => index > 0); + + readonly can_redo = map((stack, index) => index < stack.length, this.stack, this.index); + + readonly first_undo = this.can_undo.map(can_undo => { + return can_undo ? this.stack.get(this.index.val - 1) : undefined; + }); + + readonly first_redo = this.can_redo.map(can_redo => { + return can_redo ? this.stack.get(this.index.val) : undefined; + }); + + push_action(description: string, undo: () => void, redo: () => void): void { + this.push(new Action(description, undo, redo)); + } + + push(action: Action): void { + this.stack.splice(this.index.val, this.stack.length.val - this.index.val, action); + this.index.update(i => i + 1); + } + + /** + * Pop an action off the stack without undoing. + */ + pop(): Action | undefined { + this.index.update(i => i - 1); + return this.stack.splice(this.index.val, 1)[0]; + } + + undo(): boolean { + if (this.can_undo) { + this.index.update(i => i - 1); + this.stack.get(this.index.val).undo(); + return true; + } else { + return false; + } + } + + redo(): boolean { + if (this.can_redo) { + this.stack.get(this.index.val).redo(); + this.index.update(i => i + 1); + return true; + } else { + return false; + } + } + + reset(): void { + this.stack.clear(); + this.index.val = 0; + } +} diff --git a/src/core/undo/index.test.ts b/src/core/undo/index.test.ts new file mode 100644 index 00000000..c36cfa5b --- /dev/null +++ b/src/core/undo/index.test.ts @@ -0,0 +1,70 @@ +import { Action } from "./Action"; +import { UndoStack } from "./UndoStack"; + +test("simple properties and invariants", () => { + const stack = new UndoStack(); + + expect(stack.can_undo.val).toBe(false); + expect(stack.can_redo.val).toBe(false); + + stack.push(new Action("", () => {}, () => {})); + stack.push(new Action("", () => {}, () => {})); + stack.push(new Action("", () => {}, () => {})); + + expect(stack.can_undo.val).toBe(true); + expect(stack.can_redo.val).toBe(false); + + stack.undo(); + + expect(stack.can_undo.val).toBe(true); + expect(stack.can_redo.val).toBe(true); + + stack.undo(); + stack.undo(); + + expect(stack.can_undo.val).toBe(false); + expect(stack.can_redo.val).toBe(true); +}); + +test("undo", () => { + const stack = new UndoStack(); + + // Pretend value started and 3 and we've set it to 7 and then 13. + let value = 13; + + stack.push(new Action("X", () => (value = 3), () => (value = 7))); + stack.push(new Action("Y", () => (value = 7), () => (value = 13))); + + expect(stack.undo()).toBe(true); + expect(value).toBe(7); + + expect(stack.undo()).toBe(true); + expect(value).toBe(3); + + expect(stack.undo()).toBe(false); + expect(value).toBe(3); +}); + +test("redo", () => { + const stack = new UndoStack(); + + // Pretend value started and 3 and we've set it to 7 and then 13. + let value = 13; + + stack.push(new Action("X", () => (value = 3), () => (value = 7))); + stack.push(new Action("Y", () => (value = 7), () => (value = 13))); + + stack.undo(); + stack.undo(); + + expect(value).toBe(3); + + expect(stack.redo()).toBe(true); + expect(value).toBe(7); + + expect(stack.redo()).toBe(true); + expect(value).toBe(13); + + expect(stack.redo()).toBe(false); + expect(value).toBe(13); +}); diff --git a/src/core/undo/noop_undo.ts b/src/core/undo/noop_undo.ts new file mode 100644 index 00000000..4591d9d6 --- /dev/null +++ b/src/core/undo/noop_undo.ts @@ -0,0 +1,26 @@ +import { Undo } from "./Undo"; +import { property } from "../observable"; +import { undo_manager } from "./UndoManager"; + +export const NOOP_UNDO: Undo = { + can_redo: property(false), + can_undo: property(false), + first_redo: property(undefined), + first_undo: property(undefined), + + ensure_not_current() {}, + + make_current() { + undo_manager.current.val = this; + }, + + redo() { + return false; + }, + + reset() {}, + + undo() { + return false; + }, +}; diff --git a/src/quest_editor/domain/ObservableQuest.ts b/src/quest_editor/domain/ObservableQuest.ts new file mode 100644 index 00000000..2b2a8408 --- /dev/null +++ b/src/quest_editor/domain/ObservableQuest.ts @@ -0,0 +1,28 @@ +import { property } from "../../core/observable"; +import { WritableProperty } from "../../core/observable/WritableProperty"; +import { Episode } from "../../core/data_formats/parsing/quest/Episode"; + +export class ObservableQuest { + readonly id: WritableProperty; + readonly language: WritableProperty; + readonly name: WritableProperty; + readonly short_description: WritableProperty; + readonly long_description: WritableProperty; + readonly episode: Episode; + + constructor( + id: number, + language: number, + name: string, + short_description: string, + long_description: string, + episode: Episode, + ) { + this.id = property(id); + this.language = property(language); + this.name = property(name); + this.short_description = property(short_description); + this.long_description = property(long_description); + this.episode = episode; + } +} diff --git a/src/quest_editor/domain/ObservableQuestEntity.ts b/src/quest_editor/domain/ObservableQuestEntity.ts new file mode 100644 index 00000000..07dc3de4 --- /dev/null +++ b/src/quest_editor/domain/ObservableQuestEntity.ts @@ -0,0 +1,9 @@ +import { EntityType } from "../../core/data_formats/parsing/quest/entities"; + +export class ObservableQuestEntity { + readonly type: Type; + + constructor(type: Type) { + this.type = type; + } +} diff --git a/src/quest_editor/domain/ObservableQuestNpc.ts b/src/quest_editor/domain/ObservableQuestNpc.ts new file mode 100644 index 00000000..f9e63da4 --- /dev/null +++ b/src/quest_editor/domain/ObservableQuestNpc.ts @@ -0,0 +1,8 @@ +import { ObservableQuestEntity } from "./ObservableQuestEntity"; +import { NpcType } from "../../core/data_formats/parsing/quest/npc_types"; + +export class ObservableQuestNpc extends ObservableQuestEntity { + constructor(type: NpcType) { + super(type); + } +} diff --git a/src/quest_editor/domain/ObservableQuestObject.ts b/src/quest_editor/domain/ObservableQuestObject.ts new file mode 100644 index 00000000..5e1b9055 --- /dev/null +++ b/src/quest_editor/domain/ObservableQuestObject.ts @@ -0,0 +1,8 @@ +import { ObservableQuestEntity } from "./ObservableQuestEntity"; +import { ObjectType } from "../../core/data_formats/parsing/quest/object_types"; + +export class ObservableQuestObject extends ObservableQuestEntity { + constructor(type: ObjectType) { + super(type); + } +} diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts new file mode 100644 index 00000000..40f1784b --- /dev/null +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -0,0 +1,6 @@ +import { ResizableView } from "../../core/gui/ResizableView"; +import { el } from "../../core/gui/dom"; + +export class NpcCountsView extends ResizableView { + readonly element = el("div"); +} diff --git a/src/quest_editor/gui/QuesInfoView.ts b/src/quest_editor/gui/QuesInfoView.ts new file mode 100644 index 00000000..50520e43 --- /dev/null +++ b/src/quest_editor/gui/QuesInfoView.ts @@ -0,0 +1,39 @@ +import { ResizableView } from "../../core/gui/ResizableView"; +import { el } from "../../core/gui/dom"; +import { quest_editor_store } from "../stores/QuestEditorStore"; +import { Episode } from "../../core/data_formats/parsing/quest/Episode"; + +export class QuesInfoView extends ResizableView { + readonly element = el("div", { class: "quest_editor_QuesInfoView" }); + + private readonly table_element = el("table"); + private readonly episode_element: HTMLElement; + private readonly id_element: HTMLElement; + private readonly name_element: HTMLElement; + + constructor() { + super(); + + const quest = quest_editor_store.current_quest; + + this.bind_hidden(this.table_element, quest.map(q => q == undefined)); + + this.table_element.append( + el("tr", {}, el("th", { text: "Episode:" }), (this.episode_element = el("td"))), + el("tr", {}, el("th", { text: "ID:" }), (this.id_element = el("td"))), + el("tr", {}, el("th", { text: "Name:" }), (this.name_element = el("td"))), + ); + + this.element.append(this.table_element); + + this.disposables( + quest.observe(q => { + if (q) { + this.episode_element.textContent = Episode[q.episode]; + this.id_element.textContent = q.id.val.toString(); + this.name_element.textContent = q.name.val; + } + }), + ); + } +} diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 9cdeaf39..807a4bf2 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -1,20 +1,33 @@ import { ResizableView } from "../../core/gui/ResizableView"; -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import { ToolBarView } from "./ToolBarView"; -import GoldenLayout, { ContentItem } from "golden-layout"; +import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; -import { AssemblyEditorComponent } from "../../old/quest_editor/ui/AssemblyEditorComponent"; -import { quest_editor_store } from "../../old/quest_editor/stores/QuestEditorStore"; +import { QuesInfoView } from "./QuesInfoView"; import Logger = require("js-logger"); +import "golden-layout/src/css/goldenlayout-base.css"; +import "../../core/gui/golden_layout_theme.css"; +import { NpcCountsView } from "./NpcCountsView"; const logger = Logger.get("quest_editor/gui/QuestEditorView"); +// Don't change these values, as they are persisted in the user's browser. +const VIEW_TO_NAME = new Map([ + [QuesInfoView, "quest_info"], + [NpcCountsView, "npc_counts"], + // [QuestRendererView, "quest_renderer"], + // [AssemblyEditorView, "assembly_editor"], + // [EntityInfoView, "entity_info"], + // [AddObjectView, "add_object"], +]); + const DEFAULT_LAYOUT_CONFIG = { settings: { showPopoutIcon: false, + showMaximiseIcon: false, }, dimensions: { - headerHeight: 28, + headerHeight: 22, }, labels: { close: "Close", @@ -24,65 +37,151 @@ const DEFAULT_LAYOUT_CONFIG = { }, }; +const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ + { + type: "row", + content: [ + { + type: "stack", + width: 3, + content: [ + { + title: "Info", + type: "component", + componentName: VIEW_TO_NAME.get(QuesInfoView), + isClosable: false, + }, + { + title: "NPC Counts", + type: "component", + componentName: VIEW_TO_NAME.get(NpcCountsView), + isClosable: false, + }, + ], + }, + // { + // type: "stack", + // width: 9, + // content: [ + // { + // title: "3D View", + // type: "component", + // componentName: Component.QuestRenderer, + // isClosable: false, + // }, + // { + // title: "Script", + // type: "component", + // componentName: Component.AssemblyEditor, + // isClosable: false, + // }, + // ], + // }, + // { + // title: "Entity", + // type: "component", + // componentName: Component.EntityInfo, + // isClosable: false, + // width: 2, + // }, + ], + }, +]; + export class QuestEditorView extends ResizableView { - readonly element = create_el("div"); + readonly element = el("div", { class: "quest_editor_QuestEditorView" }); private readonly tool_bar_view = this.disposable(new ToolBarView()); - private layout_element = create_el("div"); - // private layout: GoldenLayout; + private readonly layout_element = el("div", { class: "quest_editor_gl_container" }); + private readonly layout: Promise; constructor() { super(); - // const content = await quest_editor_ui_persister.load_layout_config( - // [...CMP_TO_NAME.values()], - // DEFAULT_LAYOUT_CONTENT, - // ); - // - // const config: GoldenLayout.Config = { - // ...DEFAULT_LAYOUT_CONFIG, - // content, - // }; - // - // try { - // this.layout = new GoldenLayout(config, this.layout_element); - // } catch (e) { - // logger.warn("Couldn't initialize golden layout with persisted layout.", e); - // - // this.layout = new GoldenLayout( - // { - // ...DEFAULT_LAYOUT_CONFIG, - // content: DEFAULT_LAYOUT_CONTENT, - // }, - // this.layout_element, - // ); - // } - // - // for (const [component, name] of CMP_TO_NAME) { - // this.layout.registerComponent(name, component); - // } - // - // this.layout.on("stateChanged", () => { - // if (this.layout) { - // quest_editor_ui_persister.persist_layout_config(this.layout.toConfig().content); - // } - // }); - // - // this.layout.on("stackCreated", (stack: ContentItem) => { - // stack.on("activeContentItemChanged", (item: ContentItem) => { - // if ("component" in item.config) { - // if (item.config.component === CMP_TO_NAME.get(AssemblyEditorComponent)) { - // quest_editor_store.script_undo.make_current(); - // } else { - // quest_editor_store.undo.make_current(); - // } - // } - // }); - // }); - // - // this.layout.init(); - this.element.append(this.tool_bar_view.element, this.layout_element); + + this.layout = this.init_golden_layout(); + } + + resize(width: number, height: number): this { + super.resize(width, height); + + const layout_height = Math.max(0, height - this.tool_bar_view.height); + this.layout_element.style.width = `${width}px`; + this.layout_element.style.height = `${layout_height}px`; + this.layout.then(layout => layout.updateSize(width, layout_height)); + + return this; + } + + dispose(): void { + super.dispose(); + this.layout.then(layout => layout.destroy()); + } + + private async init_golden_layout(): Promise { + const content = await quest_editor_ui_persister.load_layout_config( + [...VIEW_TO_NAME.values()], + DEFAULT_LAYOUT_CONTENT, + ); + + try { + return this.attempt_gl_init({ + ...DEFAULT_LAYOUT_CONFIG, + content, + }); + } catch (e) { + logger.warn("Couldn't instantiate golden layout with persisted layout.", e); + + return this.attempt_gl_init({ + ...DEFAULT_LAYOUT_CONFIG, + content: DEFAULT_LAYOUT_CONTENT, + }); + } + } + + private attempt_gl_init(config: GoldenLayout.Config): GoldenLayout { + const layout = new GoldenLayout(config, this.layout_element); + + try { + for (const [view_ctor, name] of VIEW_TO_NAME) { + layout.registerComponent(name, function(container: Container) { + const view = new view_ctor(); + + container.on("close", () => view.dispose()); + container.on("resize", () => view.resize(container.width, container.height)); + + view.resize(container.width, container.height); + + container.getElement().append(view.element); + }); + } + + layout.on("stateChanged", () => { + if (this.layout) { + quest_editor_ui_persister.persist_layout_config(layout.toConfig().content); + } + }); + + layout.on("stackCreated", (stack: ContentItem) => { + stack.on("activeContentItemChanged", (item: ContentItem) => { + // if ("component" in item.config) { + // if (item.config.component === CMP_TO_NAME.get(AssemblyEditorComponent)) { + // quest_editor_store.script_undo.make_current(); + // } else { + // quest_editor_store.undo.make_current(); + // } + // } + }); + }); + + layout.init(); + + return layout; + } catch (e) { + layout.destroy(); + throw e; + } } } diff --git a/src/quest_editor/gui/ToolBarView.ts b/src/quest_editor/gui/ToolBarView.ts index a885fa8e..e75974e0 100644 --- a/src/quest_editor/gui/ToolBarView.ts +++ b/src/quest_editor/gui/ToolBarView.ts @@ -2,6 +2,8 @@ import { View } from "../../core/gui/View"; import { ToolBar } from "../../core/gui/ToolBar"; import { FileButton } from "../../core/gui/FileButton"; import { Button } from "../../core/gui/Button"; +import { quest_editor_store } from "../stores/QuestEditorStore"; +import { undo_manager } from "../../core/undo/UndoManager"; export class ToolBarView extends View { private readonly open_file_button = new FileButton("Open file...", ".qst"); @@ -21,4 +23,24 @@ export class ToolBarView extends View { get height(): number { return this.tool_bar.height; } + + constructor() { + super(); + + this.disposables( + this.open_file_button.files.observe(files => { + if (files.length) { + quest_editor_store.open_file(files[0]); + } + }), + + this.save_as_button.enabled.bind( + quest_editor_store.current_quest.map(q => q != undefined), + ), + + this.undo_button.enabled.bind(undo_manager.can_undo), + + this.redo_button.enabled.bind(undo_manager.can_redo), + ); + } } diff --git a/src/quest_editor/persistence/QuestEditorUiPersister.ts b/src/quest_editor/persistence/QuestEditorUiPersister.ts index a4ff0bd2..800802b0 100644 --- a/src/quest_editor/persistence/QuestEditorUiPersister.ts +++ b/src/quest_editor/persistence/QuestEditorUiPersister.ts @@ -4,7 +4,7 @@ import GoldenLayout from "golden-layout"; const LAYOUT_CONFIG_KEY = "QuestEditorUiPersister.layout_config"; -class QuestEditorUiPersister extends Persister { +export class QuestEditorUiPersister extends Persister { persist_layout_config = throttle( (config: any) => { this.persist(LAYOUT_CONFIG_KEY, config); @@ -51,11 +51,11 @@ class QuestEditorUiPersister extends Persister { return false; } - if ("component" in config) { - if (!components.has(config.component)) { + if ("componentName" in config) { + if (!components.has(config.componentName)) { return false; } else { - found.add(config.component); + found.add(config.componentName); } } diff --git a/src/quest_editor/stores/QuestEditorStore.ts b/src/quest_editor/stores/QuestEditorStore.ts new file mode 100644 index 00000000..a9be6daf --- /dev/null +++ b/src/quest_editor/stores/QuestEditorStore.ts @@ -0,0 +1,122 @@ +import { property } from "../../core/observable"; +import { ObservableQuest } from "../domain/ObservableQuest"; +import { Property } from "../../core/observable/Property"; +import { read_file } from "../../core/read_file"; +import { parse_quest } from "../../core/data_formats/parsing/quest"; +import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; +import { Endianness } from "../../core/data_formats/Endianness"; +import { SimpleUndo, UndoStack } from "../../old/core/undo"; +import Logger = require("js-logger"); + +const logger = Logger.get("quest_editor/gui/QuestEditorStore"); + +export class QuestEditorStore { + readonly undo = new UndoStack(); + readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {}); + + private readonly _current_quest = property(undefined); + readonly current_quest: Property = this._current_quest; + + // TODO: notify user of problems. + open_file = async (file: File) => { + try { + const buffer = await read_file(file); + const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little)); + this.set_quest( + quest && + new ObservableQuest( + quest.id, + quest.language, + quest.name, + quest.short_description, + quest.long_description, + quest.episode, + // quest.map_designations, + // quest.objects.map( + // obj => + // new ObservableQuestObject( + // obj.type, + // obj.id, + // obj.group_id, + // obj.area_id, + // obj.section_id, + // obj.position, + // obj.rotation, + // obj.properties, + // obj.unknown, + // ), + // ), + // quest.npcs.map( + // npc => + // new ObservableQuestNpc( + // npc.type, + // npc.pso_type_id, + // npc.npc_id, + // npc.script_label, + // npc.roaming, + // npc.area_id, + // npc.section_id, + // npc.position, + // npc.rotation, + // npc.scale, + // npc.unknown, + // ), + // ), + // quest.dat_unknowns, + // quest.object_code, + // quest.shop_items, + ), + file.name, + ); + } catch (e) { + logger.error("Couldn't read file.", e); + } + }; + + private set_quest(quest?: ObservableQuest, filename?: string): void { + // this.current_quest_filename = filename; + this.undo.reset(); + this.script_undo.reset(); + + // if (quest) { + // this.current_area = area_store.get_area(quest.episode, 0); + // } else { + // this.current_area = undefined; + // } + + if (quest) { + // Load section data. + // for (const variant of quest.area_variants) { + // const sections = yield area_store.get_area_sections( + // quest.episode, + // variant.area.id, + // variant.id, + // ); + // variant.sections.replace(sections); + // + // for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) { + // try { + // this.set_section_on_quest_entity(object, sections); + // } catch (e) { + // logger.error(e); + // } + // } + // + // for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) { + // try { + // this.set_section_on_quest_entity(npc, sections); + // } catch (e) { + // logger.error(e); + // } + // } + // } + } else { + logger.error("Couldn't parse quest file."); + } + + // this.selected_entity = undefined; + this._current_quest.val = quest; + } +} + +export const quest_editor_store = new QuestEditorStore(); diff --git a/src/viewer/gui/ModelView.ts b/src/viewer/gui/ModelView.ts index 6bd64476..e01a41ed 100644 --- a/src/viewer/gui/ModelView.ts +++ b/src/viewer/gui/ModelView.ts @@ -1,4 +1,4 @@ -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; import { ToolBar } from "../../core/gui/ToolBar"; import "./ModelView.css"; @@ -18,10 +18,10 @@ const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 130; export class ModelView extends ResizableView { - readonly element = create_el("div", "viewer_ModelView"); + readonly element = el("div", { class: "viewer_ModelView" }); private tool_bar_view = this.disposable(new ToolBarView()); - private container_element = create_el("div", "viewer_ModelView_container"); + private container_element = el("div", { class: "viewer_ModelView_container" }); private model_list_view = this.disposable( new ModelSelectListView(model_store.models, model_store.current_model), ); @@ -43,7 +43,7 @@ export class ModelView extends ResizableView { this.element.append(this.tool_bar_view.element, this.container_element); - model_store.current_model.set(model_store.models[5]); + model_store.current_model.val = model_store.models[5]; this.renderer_view.start_rendering(); @@ -147,7 +147,7 @@ class ToolBarView extends View { } class ModelSelectListView extends ResizableView { - element = create_el("ul", "viewer_ModelSelectListView"); + element = el("ul", { class: "viewer_ModelSelectListView" }); set borders(borders: boolean) { if (borders) { @@ -169,10 +169,7 @@ class ModelSelectListView extends ResizableView { models.forEach((model, index) => { this.element.append( - create_el("li", undefined, li => { - li.textContent = model.name; - li.dataset["index"] = index.toString(); - }), + el("li", { text: model.name, data: { index: index.toString() } }), ); }); @@ -206,7 +203,7 @@ class ModelSelectListView extends ResizableView { const index = parseInt(e.target.dataset["index"]!, 10); this.selected_element = e.target; - this.selected.set(this.models[index]); + this.selected.val = this.models[index]; } }; } diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 694dddb8..bbfc96e4 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -1,4 +1,4 @@ -import { create_el } from "../../core/gui/dom"; +import { el } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; import { FileButton } from "../../core/gui/FileButton"; import { ToolBar } from "../../core/gui/ToolBar"; @@ -8,7 +8,7 @@ import { TextureRenderer } from "../rendering/TextureRenderer"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; export class TextureView extends ResizableView { - readonly element = create_el("div", "viewer_TextureView"); + readonly element = el("div", { class: "viewer_TextureView" }); private readonly open_file_button = new FileButton("Open file...", ".xvm"); diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index 3a7e4116..6fd43c54 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -92,14 +92,14 @@ export class ModelRenderer extends Renderer implements Disposable { this.animation = undefined; } - const nj_data = model_store.current_nj_data.get(); + const nj_data = model_store.current_nj_data.val; if (nj_data) { const { nj_object, has_skeleton } = nj_data; let mesh: Mesh; - const xvm = model_store.current_xvm.get(); + const xvm = model_store.current_xvm.val; const textures = xvm ? xvm_to_textures(xvm) : undefined; const materials = @@ -129,7 +129,7 @@ export class ModelRenderer extends Renderer implements Disposable { this.scene.add(mesh); this.skeleton_helper = new SkeletonHelper(mesh); - this.skeleton_helper.visible = model_store.show_skeleton.get(); + this.skeleton_helper.visible = model_store.show_skeleton.val; (this.skeleton_helper.material as any).linewidth = 3; this.scene.add(this.skeleton_helper); @@ -147,7 +147,7 @@ export class ModelRenderer extends Renderer implements Disposable { mixer = this.animation.mixer; } - const nj_data = model_store.current_nj_data.get(); + const nj_data = model_store.current_nj_data.val; if (!this.mesh || !(this.mesh instanceof SkinnedMesh) || !nj_motion || !nj_data) return; @@ -195,7 +195,7 @@ export class ModelRenderer extends Renderer implements Disposable { }; private animation_frame_changed = (frame: number) => { - const nj_motion = model_store.current_nj_motion.get(); + const nj_motion = model_store.current_nj_motion.val; if (this.animation && nj_motion) { const frame_count = nj_motion.frame_count; @@ -209,7 +209,7 @@ export class ModelRenderer extends Renderer implements Disposable { private update_animation_frame(): void { if (this.animation && !this.animation.action.paused) { const time = this.animation.action.time; - model_store.animation_frame.set(time * PSO_FRAME_RATE + 1); + model_store.animation_frame.val = time * PSO_FRAME_RATE + 1; } } } diff --git a/src/viewer/stores/ModelStore.ts b/src/viewer/stores/ModelStore.ts index d93f4eab..c9116733 100644 --- a/src/viewer/stores/ModelStore.ts +++ b/src/viewer/stores/ModelStore.ts @@ -91,7 +91,7 @@ export class ModelStore implements Disposable { const cursor = new ArrayBufferCursor(buffer, Endianness.Little); if (file.name.endsWith(".nj")) { - this.current_model.set(undefined); + this.current_model.val = undefined; const nj_object = parse_nj(cursor)[0]; @@ -101,7 +101,7 @@ export class ModelStore implements Disposable { has_skeleton: true, }); } else if (file.name.endsWith(".xj")) { - this.current_model.set(undefined); + this.current_model.val = undefined; const nj_object = parse_xj(cursor)[0]; @@ -111,18 +111,18 @@ export class ModelStore implements Disposable { has_skeleton: false, }); } else if (file.name.endsWith(".njm")) { - this.current_animation.set(undefined); - this._current_nj_motion.set(undefined); + this.current_animation.val = undefined; + this._current_nj_motion.val = undefined; - const nj_data = this.current_nj_data.get(); + const nj_data = this.current_nj_data.val; if (nj_data) { - this._current_nj_motion.set(parse_njm(cursor, nj_data.bone_count)); + this.animation_playing.val = true; + this._current_nj_motion.val = parse_njm(cursor, nj_data.bone_count); } } else if (file.name.endsWith(".xvm")) { if (this.current_model) { - const xvm = parse_xvm(cursor); - this._current_xvm.set(xvm); + this._current_xvm.val = parse_xvm(cursor); } } else { logger.error(`Unknown file extension in filename "${file.name}".`); @@ -133,7 +133,7 @@ export class ModelStore implements Disposable { }; private load_model = async (model?: CharacterClassModel) => { - this.current_animation.set(undefined); + this.current_animation.val = undefined; if (model) { const nj_object = await this.get_nj_object(model); @@ -145,13 +145,13 @@ export class ModelStore implements Disposable { has_skeleton: true, }); } else { - this._current_nj_data.set(undefined); + this._current_nj_data.val = undefined; } }; private set_current_nj_data(nj_data: NjData): void { - this._current_xvm.set(undefined); - this._current_nj_data.set(nj_data); + this._current_xvm.val = undefined; + this._current_nj_data.val = nj_data; } private async get_nj_object(model: CharacterClassModel): Promise { @@ -215,13 +215,13 @@ export class ModelStore implements Disposable { } private load_animation = async (animation?: CharacterClassAnimation) => { - const nj_data = this.current_nj_data.get(); + const nj_data = this.current_nj_data.val; if (nj_data && animation) { - this._current_nj_motion.set(await this.get_nj_motion(animation, nj_data.bone_count)); - this.animation_playing.set(true); + this._current_nj_motion.val = await this.get_nj_motion(animation, nj_data.bone_count); + this.animation_playing.val = true; } else { - this._current_nj_motion.set(undefined); + this._current_nj_motion.val = undefined; } }; diff --git a/src/viewer/stores/TextureStore.ts b/src/viewer/stores/TextureStore.ts index 9eed29b2..10f25359 100644 --- a/src/viewer/stores/TextureStore.ts +++ b/src/viewer/stores/TextureStore.ts @@ -15,7 +15,7 @@ export class TextureStore { load_file = async (file: File) => { try { const buffer = await read_file(file); - this._current_xvm.set(parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little))); + this._current_xvm.val = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little)); } catch (e) { logger.error("Couldn't read file.", e); } From 17400200a093634903ead432404aa448c0f82696 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 23 Aug 2019 17:00:39 +0200 Subject: [PATCH 14/50] Ported quest info view to the new GUI system. --- src/application/gui/ApplicationView.ts | 4 +- src/application/gui/MainContentView.ts | 10 +-- src/application/gui/NavigationView.css | 2 +- src/application/gui/NavigationView.ts | 14 ++-- src/core/gui/Button.css | 6 +- src/core/gui/Button.ts | 6 +- src/core/gui/CheckBox.ts | 4 +- src/core/gui/FileButton.ts | 8 +- src/core/gui/Input.css | 23 +++--- src/core/gui/Input.ts | 83 ++++++++++++++++++++ src/core/gui/Label.ts | 8 +- src/core/gui/LabelledControl.ts | 2 +- src/core/gui/LazyView.ts | 39 +++++----- src/core/gui/NumberInput.css | 2 +- src/core/gui/NumberInput.ts | 84 ++++++++------------- src/core/gui/RendererView.ts | 4 +- src/core/gui/TabContainer.css | 2 +- src/core/gui/TabContainer.ts | 16 ++-- src/core/gui/TextArea.css | 33 ++++++++ src/core/gui/TextArea.ts | 46 +++++++++++ src/core/gui/TextInput.ts | 36 +++++++++ src/core/gui/ToolBar.css | 4 + src/core/gui/ToolBar.ts | 11 ++- src/core/gui/View.ts | 8 ++ src/core/gui/dom.ts | 35 ++++++++- src/core/gui/golden_layout_theme.css | 2 +- src/{ => core/gui}/index.css | 29 ++++++- src/core/observable/DependentProperty.ts | 2 +- src/core/observable/Property.ts | 2 +- src/index.ts | 2 +- src/quest_editor/gui/NpcCountsView.ts | 4 +- src/quest_editor/gui/QuesInfoView.css | 33 ++++++++ src/quest_editor/gui/QuesInfoView.ts | 66 +++++++++++++--- src/quest_editor/gui/QuestEditorView.ts | 6 +- src/quest_editor/stores/QuestEditorStore.ts | 8 +- src/viewer/gui/ModelView.ts | 36 +++++---- src/viewer/gui/TextureView.ts | 4 +- src/viewer/rendering/ModelRenderer.ts | 9 ++- 38 files changed, 512 insertions(+), 181 deletions(-) create mode 100644 src/core/gui/Input.ts create mode 100644 src/core/gui/TextArea.css create mode 100644 src/core/gui/TextArea.ts create mode 100644 src/core/gui/TextInput.ts rename src/{ => core/gui}/index.css (56%) create mode 100644 src/quest_editor/gui/QuesInfoView.css diff --git a/src/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts index e9887564..e2fa658e 100644 --- a/src/application/gui/ApplicationView.ts +++ b/src/application/gui/ApplicationView.ts @@ -1,10 +1,10 @@ import { NavigationView } from "./NavigationView"; import { MainContentView } from "./MainContentView"; -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; export class ApplicationView extends ResizableView { - element = el("div", { class: "application_ApplicationView" }); + element = create_element("div", { class: "application_ApplicationView" }); private menu_view = this.disposable(new NavigationView()); private main_content_view = this.disposable(new MainContentView()); diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 4c46ca21..c99b5d57 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,4 +1,4 @@ -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { LazyView } from "../../core/gui/LazyView"; import { ResizableView } from "../../core/gui/ResizableView"; @@ -12,7 +12,7 @@ const TOOLS: [GuiTool, () => Promise][] = [ ]; export class MainContentView extends ResizableView { - element = el("div", { class: "application_MainContentView" }); + element = create_element("div", { class: "application_MainContentView" }); private tool_views = new Map( TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]), @@ -26,7 +26,7 @@ export class MainContentView extends ResizableView { } const tool_view = this.tool_views.get(gui_store.tool.val); - if (tool_view) tool_view.visible = true; + if (tool_view) tool_view.visible.val = true; this.disposable(gui_store.tool.observe(this.tool_changed)); } @@ -43,10 +43,10 @@ export class MainContentView extends ResizableView { private tool_changed = (new_tool: GuiTool) => { for (const tool of this.tool_views.values()) { - tool.visible = false; + tool.visible.val = false; } const new_view = this.tool_views.get(new_tool); - if (new_view) new_view.visible = true; + if (new_view) new_view.visible.val = true; }; } diff --git a/src/application/gui/NavigationView.css b/src/application/gui/NavigationView.css index 75932b7c..8d6c147d 100644 --- a/src/application/gui/NavigationView.css +++ b/src/application/gui/NavigationView.css @@ -16,7 +16,7 @@ display: inline-flex; flex-direction: row; align-items: center; - font-size: 15px; + font-size: 13px; height: 100%; padding: 0 20px; color: hsl(0, 0%, 65%); diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 5826018b..fbce0648 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -1,4 +1,4 @@ -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import "./NavigationView.css"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { View } from "../../core/gui/View"; @@ -10,7 +10,7 @@ const TOOLS: [GuiTool, string][] = [ ]; export class NavigationView extends View { - readonly element = el("div", { class: "application_NavigationView" }); + readonly element = create_element("div", { class: "application_NavigationView" }); readonly height = 30; @@ -22,7 +22,7 @@ export class NavigationView extends View { super(); this.element.style.height = `${this.height}px`; - this.element.onclick = this.click; + this.element.onmousedown = this.mousedown; for (const button of this.buttons.values()) { this.element.append(button.element); @@ -32,7 +32,7 @@ export class NavigationView extends View { this.disposable(gui_store.tool.observe(this.tool_changed)); } - private click(e: MouseEvent): void { + private mousedown(e: MouseEvent): void { if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { gui_store.tool.val = (GuiTool as any)[e.target.control.value]; } @@ -45,10 +45,10 @@ export class NavigationView extends View { } class ToolButton extends View { - element: HTMLElement = el("span"); + element: HTMLElement = create_element("span"); - private input: HTMLInputElement = el("input"); - private label: HTMLLabelElement = el("label"); + private input: HTMLInputElement = create_element("input"); + private label: HTMLLabelElement = create_element("label"); constructor(tool: GuiTool, text: string) { super(); diff --git a/src/core/gui/Button.css b/src/core/gui/Button.css index 9d05f70a..71b3b5b8 100644 --- a/src/core/gui/Button.css +++ b/src/core/gui/Button.css @@ -2,15 +2,19 @@ display: inline-flex; flex-direction: row; align-items: stretch; + align-content: stretch; box-sizing: border-box; + height: 26px; padding: 0; border: solid 1px hsl(0, 0%, 10%); color: hsl(0, 0%, 80%); outline: none; + font-size: 13px; } .core_Button .core_Button_inner { - display: flex; + flex: 1; + display: inline-flex; flex-direction: row; align-items: center; box-sizing: border-box; diff --git a/src/core/gui/Button.ts b/src/core/gui/Button.ts index 7c7a03c7..a44905aa 100644 --- a/src/core/gui/Button.ts +++ b/src/core/gui/Button.ts @@ -1,11 +1,11 @@ -import { el } from "./dom"; +import { create_element } from "./dom"; import "./Button.css"; import { Observable } from "../observable/Observable"; import { emitter } from "../observable"; import { Control } from "./Control"; export class Button extends Control { - readonly element: HTMLButtonElement = el("button", { class: "core_Button" }); + readonly element: HTMLButtonElement = create_element("button", { class: "core_Button" }); private readonly _click = emitter(); readonly click: Observable = this._click; @@ -13,7 +13,7 @@ export class Button extends Control { constructor(text: string) { super(); - this.element.append(el("span", { class: "core_Button_inner", text })); + this.element.append(create_element("span", { class: "core_Button_inner", text })); this.enabled.observe(enabled => (this.element.disabled = !enabled)); diff --git a/src/core/gui/CheckBox.ts b/src/core/gui/CheckBox.ts index ca6dcf2e..c1bf0648 100644 --- a/src/core/gui/CheckBox.ts +++ b/src/core/gui/CheckBox.ts @@ -1,10 +1,10 @@ -import { el } from "./dom"; +import { create_element } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import { property } from "../observable"; import { LabelledControl } from "./LabelledControl"; export class CheckBox extends LabelledControl { - readonly element: HTMLInputElement = el("input", { class: "core_CheckBox" }); + readonly element: HTMLInputElement = create_element("input", { class: "core_CheckBox" }); readonly checked: WritableProperty = property(false); diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index e3227f7c..1af86183 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -1,4 +1,4 @@ -import { el } from "./dom"; +import { create_element } from "./dom"; import "./FileButton.css"; import "./Button.css"; import { property } from "../observable"; @@ -6,14 +6,14 @@ import { Property } from "../observable/Property"; import { Control } from "./Control"; export class FileButton extends Control { - readonly element: HTMLLabelElement = el("label", { + readonly element: HTMLLabelElement = create_element("label", { class: "core_FileButton core_Button", }); private readonly _files = property([]); readonly files: Property = this._files; - private input: HTMLInputElement = el("input", { + private input: HTMLInputElement = create_element("input", { class: "core_FileButton_input core_Button_inner", }); @@ -31,7 +31,7 @@ export class FileButton extends Control { }; this.element.append( - el("span", { + create_element("span", { class: "core_FileButton_inner core_Button_inner", text, }), diff --git a/src/core/gui/Input.css b/src/core/gui/Input.css index 39797b95..bd047794 100644 --- a/src/core/gui/Input.css +++ b/src/core/gui/Input.css @@ -1,32 +1,35 @@ .core_Input { + display: inline-block; box-sizing: border-box; - border: solid 1px hsl(0, 0%, 25%); + height: 24px; + border: var(--input-border); } .core_Input .core_Input_inner { box-sizing: border-box; width: 100%; - height: 24px; + height: 100%; padding: 0 3px; - border: solid 1px hsl(0, 0%, 0%); - background-color: hsl(0, 0%, 12%); - color: hsl(0, 0%, 75%); + border: var(--input-inner-border); + background-color: var(--input-bg-color); + color: var(--input-text-color); outline: none; + font-size: 13px; } .core_Input:hover { - border-color: hsl(0, 0%, 35%); + border-color: var(--input-border-hover); } .core_Input:focus-within { - border-color: hsl(0, 0%, 45%); + border-color: var(--input-border-focus); } .core_Input.disabled { - border: solid 1px hsl(0, 0%, 20%); + border: var(--input-border-disabled); } .core_Input.disabled .core_Input_inner { - background-color: hsl(0, 0%, 15%); - color: var(--text-color-disabled); + color: var(--input-text-color-disabled); + background-color: var(--input-bg-color-disabled); } diff --git a/src/core/gui/Input.ts b/src/core/gui/Input.ts new file mode 100644 index 00000000..c6ca167f --- /dev/null +++ b/src/core/gui/Input.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-dupe-class-members */ +import { LabelledControl } from "./LabelledControl"; +import { create_element } from "./dom"; +import { WritableProperty } from "../observable/WritableProperty"; +import { is_any_property, Property } from "../observable/Property"; +import "./Input.css"; + +export abstract class Input extends LabelledControl { + readonly element: HTMLElement; + + readonly value: WritableProperty; + + protected readonly input: HTMLInputElement; + + protected constructor( + value: WritableProperty, + class_name: string, + input_type: string, + input_class_name: string, + label?: string, + ) { + super(label); + + this.value = value; + + this.element = create_element("span", { class: `${class_name} core_Input` }); + + this.input = create_element("input", { + class: `${input_class_name} core_Input_inner`, + }); + this.input.type = input_type; + this.input.onchange = () => (this.value.val = this.get_input_value()); + this.set_input_value(value.val); + + this.element.append(this.input); + + this.disposables( + this.value.observe(value => this.set_input_value(value)), + + this.enabled.observe(enabled => { + this.input.disabled = !enabled; + + if (enabled) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } + }), + ); + } + + protected abstract get_input_value(): T; + + protected abstract set_input_value(value: T): void; + + protected set_attr(attr: InputAttrsOfType, value?: T | Property): void; + protected set_attr( + attr: InputAttrsOfType, + value: T | Property | undefined, + convert: (value: T) => U, + ): void; + protected set_attr( + attr: InputAttrsOfType, + value?: T | Property, + convert?: (value: T) => U, + ): void { + if (value == undefined) return; + + const input = this.input as any; + const cvt = convert ? convert : (v: T) => (v as any) as U; + + if (is_any_property(value)) { + input[attr] = cvt(value.val); + this.disposable(value.observe(v => (input[attr] = cvt(v)))); + } else { + input[attr] = cvt(value); + } + } +} + +type InputAttrsOfType = { + [K in keyof HTMLInputElement]: T extends HTMLInputElement[K] ? K : never; +}[keyof HTMLInputElement]; diff --git a/src/core/gui/Label.ts b/src/core/gui/Label.ts index 785cf6d9..c26dbd78 100644 --- a/src/core/gui/Label.ts +++ b/src/core/gui/Label.ts @@ -1,12 +1,12 @@ import { View } from "./View"; -import { el } from "./dom"; +import { create_element } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import "./Label.css"; import { property } from "../observable"; import { Property } from "../observable/Property"; export class Label extends View { - readonly element = el("label", { class: "core_Label" }); + readonly element = create_element("label", { class: "core_Label" }); set for(id: string) { this.element.htmlFor = id; @@ -14,7 +14,7 @@ export class Label extends View { readonly enabled: WritableProperty = property(true); - constructor(text: string | Property) { + constructor(text: string | Property, options: { enabled?: boolean } = {}) { super(); if (typeof text === "string") { @@ -33,5 +33,7 @@ export class Label extends View { } }), ); + + if (options.enabled != undefined) this.enabled.val = options.enabled; } } diff --git a/src/core/gui/LabelledControl.ts b/src/core/gui/LabelledControl.ts index 9bd8b857..71cb6da4 100644 --- a/src/core/gui/LabelledControl.ts +++ b/src/core/gui/LabelledControl.ts @@ -2,7 +2,7 @@ import { Label } from "./Label"; import { Control } from "./Control"; export abstract class LabelledControl extends Control { - abstract readonly preferred_label_position: "left" | "right"; + abstract readonly preferred_label_position: "left" | "right" | "top" | "bottom"; private readonly _label_text: string; private _label?: Label; diff --git a/src/core/gui/LazyView.ts b/src/core/gui/LazyView.ts index 24f81132..0d08e2b9 100644 --- a/src/core/gui/LazyView.ts +++ b/src/core/gui/LazyView.ts @@ -1,29 +1,10 @@ import { View } from "./View"; -import { el } from "./dom"; +import { create_element } from "./dom"; import { Resizable } from "./Resizable"; import { ResizableView } from "./ResizableView"; export class LazyView extends ResizableView { - readonly element = el("div", { class: "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); - }); - } - } - } + readonly element = create_element("div", { class: "core_LazyView" }); private initialized = false; private view: View & Resizable | undefined; @@ -31,7 +12,21 @@ export class LazyView extends ResizableView { constructor(private create_view: () => Promise) { super(); - this.element.hidden = true; + this.visible.val = false; + + this.disposables( + this.visible.observe(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); + }); + } + }), + ); } resize(width: number, height: number): this { diff --git a/src/core/gui/NumberInput.css b/src/core/gui/NumberInput.css index b5e37ab0..33a725ec 100644 --- a/src/core/gui/NumberInput.css +++ b/src/core/gui/NumberInput.css @@ -1,3 +1,3 @@ .core_NumberInput .core_NumberInput_inner { - text-align: right; + padding-right: 1px; } \ No newline at end of file diff --git a/src/core/gui/NumberInput.ts b/src/core/gui/NumberInput.ts index e6b37bf8..dc133e39 100644 --- a/src/core/gui/NumberInput.ts +++ b/src/core/gui/NumberInput.ts @@ -1,65 +1,43 @@ -import "./NumberInput.css"; -import "./Input.css"; -import { el } from "./dom"; -import { WritableProperty } from "../observable/WritableProperty"; import { property } from "../observable"; -import { LabelledControl } from "./LabelledControl"; -import { is_any_property, Property } from "../observable/Property"; - -export class NumberInput extends LabelledControl { - readonly element = el("span", { class: "core_NumberInput core_Input" }); - - readonly value: WritableProperty = property(0); +import { Property } from "../observable/Property"; +import { Input } from "./Input"; +import "./NumberInput.css" +export class NumberInput extends Input { readonly preferred_label_position = "left"; - private readonly input: HTMLInputElement = el("input", { - class: "core_NumberInput_inner core_Input_inner", - }); - constructor( - value = 0, - label?: string, - min: number | Property = -Infinity, - max: number | Property = Infinity, - step: number | Property = 1, + value: number = 0, + options?: { + label?: string; + min?: number | Property; + max?: number | Property; + step?: number | Property; + }, ) { - super(label); - - this.input.type = "number"; - this.input.valueAsNumber = value; - - this.set_prop("min", min); - this.set_prop("max", max); - this.set_prop("step", step); - - this.input.onchange = () => (this.value.val = this.input.valueAsNumber); - - this.element.append(this.input); - - this.disposables( - this.value.observe(value => (this.input.valueAsNumber = value)), - - this.enabled.observe(enabled => { - this.input.disabled = !enabled; - - if (enabled) { - this.element.classList.remove("disabled"); - } else { - this.element.classList.add("disabled"); - } - }), + super( + property(value), + "core_NumberInput", + "number", + "core_NumberInput_inner", + options && options.label, ); - this.element.style.width = "50px"; + if (options) { + const { min, max, step } = options; + this.set_attr("min", min, String); + this.set_attr("max", max, String); + this.set_attr("step", step, String); + } + + this.element.style.width = "54px"; } - private set_prop(prop: "min" | "max" | "step", value: T | Property): void { - if (is_any_property(value)) { - this.input[prop] = String(value.val); - this.disposable(value.observe(v => (this.input[prop] = String(v)))); - } else { - this.input[prop] = String(value); - } + protected get_input_value(): number { + return this.input.valueAsNumber; + } + + protected set_input_value(value: number): void { + this.input.valueAsNumber = value; } } diff --git a/src/core/gui/RendererView.ts b/src/core/gui/RendererView.ts index 87d08175..ee68c3bb 100644 --- a/src/core/gui/RendererView.ts +++ b/src/core/gui/RendererView.ts @@ -1,9 +1,9 @@ import { ResizableView } from "./ResizableView"; -import { el } from "./dom"; +import { create_element } from "./dom"; import { Renderer } from "../rendering/Renderer"; export class RendererView extends ResizableView { - readonly element = el("div"); + readonly element = create_element("div"); constructor(private renderer: Renderer) { super(); diff --git a/src/core/gui/TabContainer.css b/src/core/gui/TabContainer.css index 9ed91b04..9b258da1 100644 --- a/src/core/gui/TabContainer.css +++ b/src/core/gui/TabContainer.css @@ -14,7 +14,7 @@ margin: 0 1px -1px 1px; background-color: hsl(0, 0%, 12%); color: hsl(0, 0%, 75%); - font-size: 15px; + font-size: 13px; } .core_TabContainer_Tab:hover { diff --git a/src/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts index 1add86fc..1db0e1da 100644 --- a/src/core/gui/TabContainer.ts +++ b/src/core/gui/TabContainer.ts @@ -1,5 +1,5 @@ import { View } from "./View"; -import { el } from "./dom"; +import { create_element } from "./dom"; import { LazyView } from "./LazyView"; import { Resizable } from "./Resizable"; import { ResizableView } from "./ResizableView"; @@ -16,19 +16,19 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; const BAR_HEIGHT = 28; export class TabContainer extends ResizableView { - readonly element = el("div", { class: "core_TabContainer" }); + readonly element = create_element("div", { class: "core_TabContainer" }); private tabs: TabInfo[] = []; - private bar_element = el("div", { class: "core_TabContainer_Bar" }); - private panes_element = el("div", { class: "core_TabContainer_Panes" }); + private bar_element = create_element("div", { class: "core_TabContainer_Bar" }); + private panes_element = create_element("div", { class: "core_TabContainer_Panes" }); constructor(...tabs: Tab[]) { super(); - this.bar_element.onclick = this.bar_click; + this.bar_element.onmousedown = this.bar_mousedown; for (const tab of tabs) { - const tab_element = el("span", { + const tab_element = create_element("span", { class: "core_TabContainer_Tab", text: tab.title, data: { key: tab.key }, @@ -72,7 +72,7 @@ export class TabContainer extends ResizableView { return this; } - private bar_click = (e: MouseEvent) => { + private bar_mousedown = (e: MouseEvent) => { if (e.target instanceof HTMLElement) { const key = e.target.dataset["key"]; if (key) this.activate(key); @@ -89,7 +89,7 @@ export class TabContainer extends ResizableView { tab.tab_element.classList.remove("active"); } - tab.lazy_view.visible = active; + tab.lazy_view.visible.val = active; } } } diff --git a/src/core/gui/TextArea.css b/src/core/gui/TextArea.css new file mode 100644 index 00000000..21f9ecd3 --- /dev/null +++ b/src/core/gui/TextArea.css @@ -0,0 +1,33 @@ +.core_TextArea { + box-sizing: border-box; + display: inline-block; + border: var(--input-border); +} + +.core_TextArea .core_TextArea_inner { + box-sizing: border-box; + vertical-align: top; + padding: 3px; + border: var(--input-inner-border); + background-color: var(--input-bg-color); + color: var(--input-text-color); + outline: none; + font-size: 13px; +} + +.core_TextArea:hover { + border-color: var(--input-border-hover); +} + +.core_TextArea:focus-within { + border-color: var(--input-border-focus); +} + +.core_TextArea.disabled { + border: var(--input-border-disabled); +} + +.core_TextArea.disabled .core_TextArea_inner { + color: var(--input-text-color-disabled); + background-color: var(--input-bg-color-disabled); +} diff --git a/src/core/gui/TextArea.ts b/src/core/gui/TextArea.ts new file mode 100644 index 00000000..f419bd4e --- /dev/null +++ b/src/core/gui/TextArea.ts @@ -0,0 +1,46 @@ +import { LabelledControl } from "./LabelledControl"; +import { el } from "./dom"; +import { property } from "../observable"; +import { WritableProperty } from "../observable/WritableProperty"; +import "./TextArea.css"; + +export class TextArea extends LabelledControl { + readonly element: HTMLElement = el.div({ class: "core_TextArea" }); + + readonly preferred_label_position = "left"; + + readonly value: WritableProperty; + + private readonly text_element: HTMLTextAreaElement = el.textarea({ + class: "core_TextArea_inner", + }); + + constructor( + value = "", + options?: { + label?: string; + max_length?: number; + font_family?: string; + rows?: number; + cols?: number; + }, + ) { + super(options && options.label); + + if (options) { + if (options.max_length != undefined) this.text_element.maxLength = options.max_length; + if (options.font_family != undefined) + this.text_element.style.fontFamily = options.font_family; + if (options.rows != undefined) this.text_element.rows = options.rows; + if (options.cols != undefined) this.text_element.cols = options.cols; + } + + this.value = property(value); + + this.text_element.onchange = () => (this.value.val = this.text_element.value); + + this.disposables(this.value.observe(value => (this.text_element.value = value))); + + this.element.append(this.text_element); + } +} diff --git a/src/core/gui/TextInput.ts b/src/core/gui/TextInput.ts new file mode 100644 index 00000000..74e0e953 --- /dev/null +++ b/src/core/gui/TextInput.ts @@ -0,0 +1,36 @@ +import { Input } from "./Input"; +import { Property } from "../observable/Property"; +import { property } from "../observable"; + +export class TextInput extends Input { + readonly preferred_label_position = "left"; + + constructor( + value = "", + options?: { + label?: string; + max_length?: number | Property; + }, + ) { + super( + property(value), + "core_TextInput", + "text", + "core_TextInput_inner", + options && options.label, + ); + + if (options) { + const { max_length } = options; + this.set_attr("maxLength", max_length); + } + } + + protected get_input_value(): string { + return this.input.value; + } + + protected set_input_value(value: string): void { + this.input.value = value; + } +} diff --git a/src/core/gui/ToolBar.css b/src/core/gui/ToolBar.css index d629ebd1..6ada06c7 100644 --- a/src/core/gui/ToolBar.css +++ b/src/core/gui/ToolBar.css @@ -20,3 +20,7 @@ .core_ToolBar > .core_ToolBar_group > * { margin: 0 2px; } + +.core_ToolBar .core_Input { + height: 26px; +} diff --git a/src/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index 1c0aeb41..ac48917f 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -1,10 +1,10 @@ import { View } from "./View"; -import { el } from "./dom"; +import { create_element } from "./dom"; import "./ToolBar.css"; import { LabelledControl } from "./LabelledControl"; export class ToolBar extends View { - readonly element = el("div", { class: "core_ToolBar" }); + readonly element = create_element("div", { class: "core_ToolBar" }); readonly height = 33; constructor(...children: View[]) { @@ -14,9 +14,12 @@ export class ToolBar extends View { for (const child of children) { if (child instanceof LabelledControl) { - const group = el("div", { class: "core_ToolBar_group" }); + const group = create_element("div", { class: "core_ToolBar_group" }); - if (child.preferred_label_position === "left") { + if ( + child.preferred_label_position === "left" || + child.preferred_label_position === "top" + ) { group.append(child.label.element, child.element); } else { group.append(child.element, child.label.element); diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts index 3bc31f53..e2dd2ebc 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/View.ts @@ -2,6 +2,8 @@ import { Disposable } from "../observable/Disposable"; import { Disposer } from "../observable/Disposer"; import { Observable } from "../observable/Observable"; import { bind_hidden } from "./dom"; +import { WritableProperty } from "../observable/WritableProperty"; +import { property } from "../observable"; export abstract class View implements Disposable { abstract readonly element: HTMLElement; @@ -14,8 +16,14 @@ export abstract class View implements Disposable { this.element.id = id; } + readonly visible: WritableProperty = property(true); + private disposer = new Disposer(); + constructor() { + this.disposables(this.visible.observe(visible => (this.element.hidden = !visible))); + } + dispose(): void { this.element.remove(); this.disposer.dispose(); diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 3bb42a36..69a1452d 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -2,16 +2,41 @@ import { Disposable } from "../observable/Disposable"; import { Observable } from "../observable/Observable"; import { is_property } from "../observable/Property"; -export function el( +export const el = { + div: (attributes?: {}, ...children: HTMLElement[]): HTMLDivElement => + create_element("div", attributes, ...children), + + table: (attributes?: {}, ...children: HTMLElement[]): HTMLTableElement => + create_element("table", attributes, ...children), + + tr: (attributes?: {}, ...children: HTMLElement[]): HTMLTableRowElement => + create_element("tr", attributes, ...children), + + th: ( + attributes?: { text?: string; col_span?: number }, + ...children: HTMLElement[] + ): HTMLTableHeaderCellElement => create_element("th", attributes, ...children), + + td: ( + attributes?: { text?: string; col_span?: number }, + ...children: HTMLElement[] + ): HTMLTableCellElement => create_element("td", attributes, ...children), + + textarea: (attributes?: {}, ...children: HTMLElement[]): HTMLTextAreaElement => + create_element("textarea", attributes, ...children), +}; + +export function create_element( tag_name: string, attributes?: { class?: string; - text?: string ; + text?: string; data?: { [key: string]: string }; + col_span?: number; }, ...children: HTMLElement[] ): T { - const element = document.createElement(tag_name) as T; + const element = document.createElement(tag_name) as HTMLTableCellElement; if (attributes) { if (attributes.class) element.className = attributes.class; @@ -22,11 +47,13 @@ export function el( element.dataset[key] = val; } } + + if (attributes.col_span) element.colSpan = attributes.col_span; } element.append(...children); - return element; + return (element as HTMLElement) as T; } export function bind_hidden(element: HTMLElement, observable: Observable): Disposable { diff --git a/src/core/gui/golden_layout_theme.css b/src/core/gui/golden_layout_theme.css index 9a23367c..5f1c6a9b 100644 --- a/src/core/gui/golden_layout_theme.css +++ b/src/core/gui/golden_layout_theme.css @@ -17,7 +17,7 @@ margin: 0 1px -1px 1px; background-color: hsl(0, 0%, 12%); color: hsl(0, 0%, 75%); - font-size: 15px; + font-size: 13px; } #root .lm_tab:hover { diff --git a/src/index.css b/src/core/gui/index.css similarity index 56% rename from src/index.css rename to src/core/gui/index.css index b8475e89..cb7c7931 100644 --- a/src/index.css +++ b/src/core/gui/index.css @@ -1,11 +1,28 @@ :root { + /* Basic view variables */ + --bg-color: hsl(0, 0%, 15%); --text-color: hsl(0, 0%, 80%); --text-color-disabled: hsl(0, 0%, 55%); --border-color: hsl(0, 0%, 25%); + /* Scrollbars */ + --scrollbar-color: hsl(0, 0%, 13%); --scrollbar-thumb-color: hsl(0, 0%, 17%); + + /* Inputs */ + + --input-bg-color: hsl(0, 0%, 12%); + --input-bg-color-disabled: hsl(0, 0%, 15%); + --input-text-color: hsl(0, 0%, 75%); + --input-text-color-disabled: var(--text-color-disabled); + --input-border: solid 1px hsl(0, 0%, 25%); + --input-border-hover: hsl(0, 0%, 35%); + --input-border-focus: hsl(0, 0%, 45%); + --input-border-disabled: solid 1px hsl(0, 0%, 20%); + + --input-inner-border: solid 1px hsl(0, 0%, 5%); } * { @@ -33,9 +50,15 @@ body { user-select: none; overflow: hidden; margin: 0; - font-size: 15px; - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, - Arial, sans-serif; + font-size: 13px; background-color: var(--bg-color); color: var(--text-color); } + +* { + font-family: Verdana, Geneva, sans-serif; +} + +#root *[hidden] { + display: none; +} diff --git a/src/core/observable/DependentProperty.ts b/src/core/observable/DependentProperty.ts index 494c494c..54dd806c 100644 --- a/src/core/observable/DependentProperty.ts +++ b/src/core/observable/DependentProperty.ts @@ -15,7 +15,7 @@ export class DependentProperty extends AbstractMinimalProperty implements private _val?: T; get val(): T { - if (this.dependency_disposables) { + if (this.dependency_disposables.length) { return this._val as T; } else { return this.f(); diff --git a/src/core/observable/Property.ts b/src/core/observable/Property.ts index 9efbc1a7..01310d8b 100644 --- a/src/core/observable/Property.ts +++ b/src/core/observable/Property.ts @@ -15,5 +15,5 @@ export function is_property(observable: Observable): observable is Propert } export function is_any_property(observable: any): observable is Property { - return (observable as any).is_property; + return observable && (observable as any).is_property; } diff --git a/src/index.ts b/src/index.ts index 1bd1e9b8..7f4525e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { ApplicationView } from "./application/gui/ApplicationView"; import { Disposable } from "./core/observable/Disposable"; -import "./index.css"; +import "./core/gui/index.css"; import { throttle } from "lodash"; import Logger from "js-logger"; diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts index 40f1784b..aa877d8d 100644 --- a/src/quest_editor/gui/NpcCountsView.ts +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -1,6 +1,6 @@ import { ResizableView } from "../../core/gui/ResizableView"; -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; export class NpcCountsView extends ResizableView { - readonly element = el("div"); + readonly element = create_element("div"); } diff --git a/src/quest_editor/gui/QuesInfoView.css b/src/quest_editor/gui/QuesInfoView.css new file mode 100644 index 00000000..e44a7909 --- /dev/null +++ b/src/quest_editor/gui/QuesInfoView.css @@ -0,0 +1,33 @@ +.quest_editor_QuesInfoView { + box-sizing: border-box; + padding: 3px; + overflow: auto; +} + +.quest_editor_QuesInfoView table { + width: 100%; +} + +.quest_editor_QuesInfoView th { + text-align: left; +} + +.quest_editor_QuesInfoView .core_TextInput { + width: 100%; +} + +.quest_editor_QuesInfoView .core_TextArea { + width: 100%; +} + +.quest_editor_QuesInfoView textarea { + width: 100%; +} + +.quest_editor_QuesInfoView_no_quest { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} diff --git a/src/quest_editor/gui/QuesInfoView.ts b/src/quest_editor/gui/QuesInfoView.ts index 50520e43..9c7d5d5a 100644 --- a/src/quest_editor/gui/QuesInfoView.ts +++ b/src/quest_editor/gui/QuesInfoView.ts @@ -2,36 +2,78 @@ import { ResizableView } from "../../core/gui/ResizableView"; import { el } from "../../core/gui/dom"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; +import { NumberInput } from "../../core/gui/NumberInput"; +import { Disposer } from "../../core/observable/Disposer"; +import { TextInput } from "../../core/gui/TextInput"; +import { TextArea } from "../../core/gui/TextArea"; +import "./QuesInfoView.css"; +import { Label } from "../../core/gui/Label"; export class QuesInfoView extends ResizableView { - readonly element = el("div", { class: "quest_editor_QuesInfoView" }); + readonly element = el.div({ class: "quest_editor_QuesInfoView" }); - private readonly table_element = el("table"); + private readonly table_element = el.table(); private readonly episode_element: HTMLElement; - private readonly id_element: HTMLElement; - private readonly name_element: HTMLElement; + private readonly id_input = this.disposable(new NumberInput()); + private readonly name_input = this.disposable(new TextInput()); + private readonly short_description_input = this.disposable( + new TextArea("", { + max_length: 128, + font_family: '"Courier New", monospace', + cols: 25, + rows: 5, + }), + ); + private readonly long_description_input = this.disposable( + new TextArea("", { + max_length: 288, + font_family: '"Courier New", monospace', + cols: 25, + rows: 10, + }), + ); + + private readonly no_quest_element = el.div({ class: "quest_editor_QuesInfoView_no_quest" }); + private readonly no_quest_label = this.disposable( + new Label("No quest loaded.", { enabled: false }), + ); + + private readonly quest_disposer = this.disposable(new Disposer()); constructor() { super(); const quest = quest_editor_store.current_quest; - this.bind_hidden(this.table_element, quest.map(q => q == undefined)); + this.no_quest_element.append(this.no_quest_label.element); + this.bind_hidden(this.no_quest_element, quest.map(q => q != undefined)); this.table_element.append( - el("tr", {}, el("th", { text: "Episode:" }), (this.episode_element = el("td"))), - el("tr", {}, el("th", { text: "ID:" }), (this.id_element = el("td"))), - el("tr", {}, el("th", { text: "Name:" }), (this.name_element = el("td"))), + el.tr({}, el.th({ text: "Episode:" }), (this.episode_element = el.td())), + el.tr({}, el.th({ text: "ID:" }), el.td({}, this.id_input.element)), + el.tr({}, el.th({ text: "Name:" }), el.td({}, this.name_input.element)), + el.tr({}, el.th({ text: "Short description:", col_span: 2 })), + el.tr({}, el.td({ col_span: 2 }, this.short_description_input.element)), + el.tr({}, el.th({ text: "Long description:", col_span: 2 })), + el.tr({}, el.td({ col_span: 2 }, this.long_description_input.element)), ); + this.bind_hidden(this.table_element, quest.map(q => q == undefined)); - this.element.append(this.table_element); + this.element.append(this.table_element, this.no_quest_element); this.disposables( quest.observe(q => { + this.quest_disposer.dispose(); + + this.episode_element.textContent = q ? Episode[q.episode] : ""; + if (q) { - this.episode_element.textContent = Episode[q.episode]; - this.id_element.textContent = q.id.val.toString(); - this.name_element.textContent = q.name.val; + this.quest_disposer.add_all( + this.id_input.value.bind_bi(q.id), + this.name_input.value.bind_bi(q.name), + this.short_description_input.value.bind_bi(q.short_description), + this.long_description_input.value.bind_bi(q.long_description), + ); } }), ); diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 807a4bf2..0f6480e8 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -1,5 +1,5 @@ import { ResizableView } from "../../core/gui/ResizableView"; -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import { ToolBarView } from "./ToolBarView"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; @@ -89,11 +89,11 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ ]; export class QuestEditorView extends ResizableView { - readonly element = el("div", { class: "quest_editor_QuestEditorView" }); + readonly element = create_element("div", { class: "quest_editor_QuestEditorView" }); private readonly tool_bar_view = this.disposable(new ToolBarView()); - private readonly layout_element = el("div", { class: "quest_editor_gl_container" }); + private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout: Promise; constructor() { diff --git a/src/quest_editor/stores/QuestEditorStore.ts b/src/quest_editor/stores/QuestEditorStore.ts index a9be6daf..8956ca46 100644 --- a/src/quest_editor/stores/QuestEditorStore.ts +++ b/src/quest_editor/stores/QuestEditorStore.ts @@ -6,14 +6,20 @@ import { parse_quest } from "../../core/data_formats/parsing/quest"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { Endianness } from "../../core/data_formats/Endianness"; import { SimpleUndo, UndoStack } from "../../old/core/undo"; +import { WritableProperty } from "../../core/observable/WritableProperty"; import Logger = require("js-logger"); const logger = Logger.get("quest_editor/gui/QuestEditorStore"); export class QuestEditorStore { + readonly debug: WritableProperty = property(false); + readonly undo = new UndoStack(); readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {}); + private readonly _current_quest_filename = property(undefined); + readonly current_quest_filename: Property = this._current_quest_filename; + private readonly _current_quest = property(undefined); readonly current_quest: Property = this._current_quest; @@ -74,7 +80,7 @@ export class QuestEditorStore { }; private set_quest(quest?: ObservableQuest, filename?: string): void { - // this.current_quest_filename = filename; + this._current_quest_filename.val = filename; this.undo.reset(); this.script_undo.reset(); diff --git a/src/viewer/gui/ModelView.ts b/src/viewer/gui/ModelView.ts index e01a41ed..979d27a7 100644 --- a/src/viewer/gui/ModelView.ts +++ b/src/viewer/gui/ModelView.ts @@ -1,4 +1,4 @@ -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; import { ToolBar } from "../../core/gui/ToolBar"; import "./ModelView.css"; @@ -18,10 +18,10 @@ const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 130; export class ModelView extends ResizableView { - readonly element = el("div", { class: "viewer_ModelView" }); + readonly element = create_element("div", { class: "viewer_ModelView" }); private tool_bar_view = this.disposable(new ToolBarView()); - private container_element = el("div", { class: "viewer_ModelView_container" }); + private container_element = create_element("div", { class: "viewer_ModelView_container" }); private model_list_view = this.disposable( new ModelSelectListView(model_store.models, model_store.current_model), ); @@ -78,20 +78,18 @@ class ToolBarView extends View { private readonly open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm"); private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton"); private readonly play_animation_checkbox = new CheckBox(true, "Play animation"); - private readonly animation_frame_rate_input = new NumberInput( - PSO_FRAME_RATE, - "Frame rate:", - 1, - 240, - 1, - ); - private readonly animation_frame_input = new NumberInput( - 1, - "Frame:", - 1, - model_store.animation_frame_count, - 1, - ); + private readonly animation_frame_rate_input = new NumberInput(PSO_FRAME_RATE, { + label: "Frame rate:", + min: 1, + max: 240, + step: 1, + }); + private readonly animation_frame_input = new NumberInput(1, { + label: "Frame:", + min: 1, + max: model_store.animation_frame_count, + step: 1, + }); private readonly animation_frame_count_label = new Label( model_store.animation_frame_count.map(count => `/ ${count}`), ); @@ -147,7 +145,7 @@ class ToolBarView extends View { } class ModelSelectListView extends ResizableView { - element = el("ul", { class: "viewer_ModelSelectListView" }); + element = create_element("ul", { class: "viewer_ModelSelectListView" }); set borders(borders: boolean) { if (borders) { @@ -169,7 +167,7 @@ class ModelSelectListView extends ResizableView { models.forEach((model, index) => { this.element.append( - el("li", { text: model.name, data: { index: index.toString() } }), + create_element("li", { text: model.name, data: { index: index.toString() } }), ); }); diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index bbfc96e4..7b611bde 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -1,4 +1,4 @@ -import { el } from "../../core/gui/dom"; +import { create_element } from "../../core/gui/dom"; import { ResizableView } from "../../core/gui/ResizableView"; import { FileButton } from "../../core/gui/FileButton"; import { ToolBar } from "../../core/gui/ToolBar"; @@ -8,7 +8,7 @@ import { TextureRenderer } from "../rendering/TextureRenderer"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; export class TextureView extends ResizableView { - readonly element = el("div", { class: "viewer_TextureView" }); + readonly element = create_element("div", { class: "viewer_TextureView" }); private readonly open_file_button = new FileButton("Open file...", ".xvm"); diff --git a/src/viewer/rendering/ModelRenderer.ts b/src/viewer/rendering/ModelRenderer.ts index 6fd43c54..84e41a81 100644 --- a/src/viewer/rendering/ModelRenderer.ts +++ b/src/viewer/rendering/ModelRenderer.ts @@ -36,6 +36,7 @@ export class ModelRenderer extends Renderer implements Disposable { clip: AnimationClip; action: AnimationAction; }; + private update_animation_time = true; constructor() { super(new PerspectiveCamera(75, 1, 1, 200)); @@ -201,7 +202,11 @@ export class ModelRenderer extends Renderer implements Disposable { const frame_count = nj_motion.frame_count; if (frame > frame_count) frame = 1; if (frame < 1) frame = frame_count; - this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; + + if (this.update_animation_time) { + this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; + } + this.schedule_render(); } }; @@ -209,7 +214,9 @@ export class ModelRenderer extends Renderer implements Disposable { private update_animation_frame(): void { if (this.animation && !this.animation.action.paused) { const time = this.animation.action.time; + this.update_animation_time = false; model_store.animation_frame.val = time * PSO_FRAME_RATE + 1; + this.update_animation_time = true; } } } From 03dc60cec9e5c247247fb1d4a095ddc727461e66 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Mon, 26 Aug 2019 15:42:12 +0200 Subject: [PATCH 15/50] Undo/redo now works again in the quest editor. The NPC counts view is also ported. --- src/application/gui/MainContentView.ts | 3 +- src/application/gui/NavigationView.ts | 6 +- src/core/data_formats/parsing/quest/bin.ts | 10 +- src/core/data_formats/parsing/quest/index.ts | 4 +- .../data_formats/parsing/quest/npc_types.ts | 46 ++-- src/core/gui/Button.ts | 4 +- src/core/gui/CheckBox.ts | 4 +- src/core/gui/FileButton.ts | 18 +- src/core/gui/Input.ts | 10 +- src/core/gui/Label.ts | 6 +- src/core/gui/LazyView.ts | 4 +- src/core/gui/TextArea.ts | 2 +- src/core/gui/View.ts | 2 +- src/core/gui/dom.ts | 2 +- .../observable/AbstractMinimalProperty.ts | 31 ++- src/core/observable/DependentProperty.ts | 13 +- src/core/observable/Disposable.ts | 7 + src/core/observable/Disposer.test.ts | 51 ++++ src/core/observable/Disposer.ts | 34 ++- src/core/observable/Emitter.ts | 6 +- src/core/observable/FlatMappedProperty.ts | 19 +- src/core/observable/Observable.ts | 8 +- src/core/observable/Property.ts | 14 +- src/core/observable/SimpleEmitter.ts | 10 +- src/core/observable/SimpleProperty.ts | 26 +- .../observable/SimpleWritableArrayProperty.ts | 24 +- src/core/observable/WritableProperty.ts | 12 +- src/{old => }/core/primitive_conversion.ts | 0 src/core/rendering/Renderer.ts | 2 +- src/core/stores/GuiStore.ts | 2 +- src/core/undo/Action.ts | 10 +- src/core/undo/SimpleUndo.ts | 14 +- .../undo/{index.test.ts => UndoStack.test.ts} | 15 +- src/core/undo/UndoStack.ts | 63 +++-- src/old/core/undo.test.ts | 69 ----- .../domain/ObservableAreaVariant.ts | 17 -- .../quest_editor/domain/ObservableQuest.ts | 178 ------------- .../domain/observable_quest_entities.ts | 14 +- .../quest_editor/stores/QuestEditorStore.ts | 20 +- .../ui/AssemblyEditorComponent.tsx | 2 +- .../quest_editor/ui/EntityInfoComponent.tsx | 4 +- .../quest_editor/ui/QuestInfoComponent.css | 17 -- .../quest_editor/ui/QuestInfoComponent.tsx | 126 --------- .../ui/QuestRendererComponent.tsx | 26 -- src/quest_editor/actions/EditIdAction.ts | 13 + .../actions/EditLongDescriptionAction.ts | 13 + src/quest_editor/actions/EditNameAction.ts | 13 + .../actions/EditShortDescriptionAction.ts | 13 + src/quest_editor/actions/QuestEditAction.ts | 19 ++ .../actions/TranslateEntityAction.ts | 27 ++ src/quest_editor/domain/ObservableQuest.ts | 28 -- .../domain/ObservableQuestEntity.ts | 9 - src/quest_editor/domain/ObservableQuestNpc.ts | 8 - .../domain/ObservableQuestObject.ts | 8 - src/quest_editor/gui/NpcCountsView.css | 27 ++ src/quest_editor/gui/NpcCountsView.ts | 67 ++++- src/quest_editor/gui/QuesInfoView.ts | 29 +- src/quest_editor/gui/QuestEditorView.ts | 61 +++-- src/quest_editor/gui/QuestRendererView.ts | 37 +++ src/quest_editor/gui/ToolBarView.ts | 10 +- .../quest_editor/loading/LoadingCache.ts | 0 src/{old => }/quest_editor/loading/areas.ts | 22 +- .../quest_editor/loading/entities.ts | 20 +- .../model/AreaModel.ts} | 8 +- src/quest_editor/model/AreaVariantModel.ts | 22 ++ src/quest_editor/model/QuestEntityModel.ts | 104 ++++++++ src/quest_editor/model/QuestModel.ts | 194 ++++++++++++++ src/quest_editor/model/QuestNpcModel.ts | 38 +++ src/quest_editor/model/QuestObjectModel.ts | 25 ++ .../model/SectionModel.ts} | 4 +- .../rendering/QuestEntityControls.ts | 98 ++++--- .../rendering/QuestModelManager.ts | 71 ++--- .../quest_editor/rendering/QuestRenderer.ts | 65 ++--- .../rendering/conversion/areas.ts | 20 +- .../rendering/conversion/entities.ts | 26 +- .../scripting/AssemblyAnalyser.ts | 22 +- .../scripting/AssemblyLexer.test.ts | 0 .../quest_editor/scripting/AssemblyLexer.ts | 0 .../quest_editor/scripting/assembly.test.ts | 0 .../quest_editor/scripting/assembly.ts | 2 +- .../quest_editor/scripting/assembly_worker.ts | 0 .../scripting/assembly_worker_messages.ts | 0 .../ControlFlowGraph.test.ts | 0 .../data_flow_analysis/ControlFlowGraph.ts | 0 .../data_flow_analysis/ValueSet.test.ts | 0 .../scripting/data_flow_analysis/ValueSet.ts | 0 .../data_flow_analysis/register_value.test.ts | 0 .../data_flow_analysis/register_value.ts | 2 +- .../data_flow_analysis/stack_value.ts | 2 +- .../scripting/disassembly.test.ts | 10 +- .../quest_editor/scripting/disassembly.ts | 0 .../quest_editor/scripting/instructions.ts | 0 .../quest_editor/scripting/opcodes.ts | 0 .../quest_editor/scripting/vm/index.ts | 2 +- .../quest_editor/stores/AreaStore.ts | 24 +- src/quest_editor/stores/QuestEditorStore.ts | 252 ++++++++++++------ .../quest_editor/stores/quest_creation.ts | 105 ++++---- .../gui/{ModelView.css => Model3DView.css} | 2 +- .../gui/{ModelView.ts => Model3DView.ts} | 38 +-- src/viewer/gui/TextureView.ts | 4 +- src/viewer/gui/ViewerView.ts | 2 +- .../CharacterClassAnimationModel.ts} | 2 +- .../{domain => model}/CharacterClassModel.ts | 0 .../{ModelRenderer.ts => Model3DRenderer.ts} | 15 +- src/viewer/rendering/TextureRenderer.ts | 2 +- .../stores/{ModelStore.ts => Model3DStore.ts} | 22 +- webpack.dev.js | 2 +- 107 files changed, 1501 insertions(+), 1063 deletions(-) create mode 100644 src/core/observable/Disposer.test.ts rename src/{old => }/core/primitive_conversion.ts (100%) rename src/core/undo/{index.test.ts => UndoStack.test.ts} (69%) delete mode 100644 src/old/core/undo.test.ts delete mode 100644 src/old/quest_editor/domain/ObservableAreaVariant.ts delete mode 100644 src/old/quest_editor/domain/ObservableQuest.ts delete mode 100644 src/old/quest_editor/ui/QuestInfoComponent.css delete mode 100644 src/old/quest_editor/ui/QuestInfoComponent.tsx delete mode 100644 src/old/quest_editor/ui/QuestRendererComponent.tsx create mode 100644 src/quest_editor/actions/EditIdAction.ts create mode 100644 src/quest_editor/actions/EditLongDescriptionAction.ts create mode 100644 src/quest_editor/actions/EditNameAction.ts create mode 100644 src/quest_editor/actions/EditShortDescriptionAction.ts create mode 100644 src/quest_editor/actions/QuestEditAction.ts create mode 100644 src/quest_editor/actions/TranslateEntityAction.ts delete mode 100644 src/quest_editor/domain/ObservableQuest.ts delete mode 100644 src/quest_editor/domain/ObservableQuestEntity.ts delete mode 100644 src/quest_editor/domain/ObservableQuestNpc.ts delete mode 100644 src/quest_editor/domain/ObservableQuestObject.ts create mode 100644 src/quest_editor/gui/NpcCountsView.css create mode 100644 src/quest_editor/gui/QuestRendererView.ts rename src/{old => }/quest_editor/loading/LoadingCache.ts (100%) rename src/{old => }/quest_editor/loading/areas.ts (87%) rename src/{old => }/quest_editor/loading/entities.ts (91%) rename src/{old/quest_editor/domain/ObservableArea.ts => quest_editor/model/AreaModel.ts} (76%) create mode 100644 src/quest_editor/model/AreaVariantModel.ts create mode 100644 src/quest_editor/model/QuestEntityModel.ts create mode 100644 src/quest_editor/model/QuestModel.ts create mode 100644 src/quest_editor/model/QuestNpcModel.ts create mode 100644 src/quest_editor/model/QuestObjectModel.ts rename src/{old/quest_editor/domain/Section.ts => quest_editor/model/SectionModel.ts} (90%) rename src/{old => }/quest_editor/rendering/QuestEntityControls.ts (83%) rename src/{old => }/quest_editor/rendering/QuestModelManager.ts (78%) rename src/{old => }/quest_editor/rendering/QuestRenderer.ts (64%) rename src/{old => }/quest_editor/rendering/conversion/areas.ts (85%) rename src/{old => }/quest_editor/rendering/conversion/entities.ts (73%) rename src/{old => }/quest_editor/scripting/AssemblyAnalyser.ts (91%) rename src/{old => }/quest_editor/scripting/AssemblyLexer.test.ts (100%) rename src/{old => }/quest_editor/scripting/AssemblyLexer.ts (100%) rename src/{old => }/quest_editor/scripting/assembly.test.ts (100%) rename src/{old => }/quest_editor/scripting/assembly.ts (99%) rename src/{old => }/quest_editor/scripting/assembly_worker.ts (100%) rename src/{old => }/quest_editor/scripting/assembly_worker_messages.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.test.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/ControlFlowGraph.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/ValueSet.test.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/ValueSet.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/register_value.test.ts (100%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/register_value.ts (98%) rename src/{old => }/quest_editor/scripting/data_flow_analysis/stack_value.ts (97%) rename src/{old => }/quest_editor/scripting/disassembly.test.ts (83%) rename src/{old => }/quest_editor/scripting/disassembly.ts (100%) rename src/{old => }/quest_editor/scripting/instructions.ts (100%) rename src/{old => }/quest_editor/scripting/opcodes.ts (100%) rename src/{old => }/quest_editor/scripting/vm/index.ts (99%) rename src/{old => }/quest_editor/stores/AreaStore.ts (62%) rename src/{old => }/quest_editor/stores/quest_creation.ts (92%) rename src/viewer/gui/{ModelView.css => Model3DView.css} (93%) rename src/viewer/gui/{ModelView.ts => Model3DView.ts} (85%) rename src/viewer/{domain/CharacterClassAnimation.ts => model/CharacterClassAnimationModel.ts} (59%) rename src/viewer/{domain => model}/CharacterClassModel.ts (100%) rename src/viewer/rendering/{ModelRenderer.ts => Model3DRenderer.ts} (91%) rename src/viewer/stores/{ModelStore.ts => Model3DStore.ts} (92%) diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index c99b5d57..4a1992c2 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -2,6 +2,7 @@ import { create_element } from "../../core/gui/dom"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { LazyView } from "../../core/gui/LazyView"; import { ResizableView } from "../../core/gui/ResizableView"; +import { ChangeEvent } from "../../core/observable/Observable"; const TOOLS: [GuiTool, () => Promise][] = [ [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], @@ -41,7 +42,7 @@ export class MainContentView extends ResizableView { return this; } - private tool_changed = (new_tool: GuiTool) => { + private tool_changed = ({ value: new_tool }: ChangeEvent) => { for (const tool of this.tool_views.values()) { tool.visible.val = false; } diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index fbce0648..4c245368 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -28,8 +28,8 @@ export class NavigationView extends View { this.element.append(button.element); } - this.tool_changed(gui_store.tool.val); - this.disposable(gui_store.tool.observe(this.tool_changed)); + this.mark_tool_button(gui_store.tool.val); + this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value))); } private mousedown(e: MouseEvent): void { @@ -38,7 +38,7 @@ export class NavigationView extends View { } } - private tool_changed = (tool: GuiTool) => { + private mark_tool_button = (tool: GuiTool) => { const button = this.buttons.get(tool); if (button) button.checked = true; }; diff --git a/src/core/data_formats/parsing/quest/bin.ts b/src/core/data_formats/parsing/quest/bin.ts index 117f4ff3..50950a7a 100644 --- a/src/core/data_formats/parsing/quest/bin.ts +++ b/src/core/data_formats/parsing/quest/bin.ts @@ -1,8 +1,8 @@ import Logger from "js-logger"; import { Endianness } from "../../Endianness"; -import { ControlFlowGraph } from "../../../../old/quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; -import { register_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/register_value"; -import { stack_value } from "../../../../old/quest_editor/scripting/data_flow_analysis/stack_value"; +import { ControlFlowGraph } from "../../../../quest_editor/scripting/data_flow_analysis/ControlFlowGraph"; +import { register_value } from "../../../../quest_editor/scripting/data_flow_analysis/register_value"; +import { stack_value } from "../../../../quest_editor/scripting/data_flow_analysis/stack_value"; import { Arg, DataSegment, @@ -11,13 +11,13 @@ import { Segment, SegmentType, StringSegment, -} from "../../../../old/quest_editor/scripting/instructions"; +} from "../../../../quest_editor/scripting/instructions"; import { Kind, Opcode, OPCODES, StackInteraction, -} from "../../../../old/quest_editor/scripting/opcodes"; +} from "../../../../quest_editor/scripting/opcodes"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { Cursor } from "../../cursor/Cursor"; import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 49f20ffa..6591f7ab 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -4,8 +4,8 @@ import { InstructionSegment, Segment, SegmentType, -} from "../../../../old/quest_editor/scripting/instructions"; -import { Opcode } from "../../../../old/quest_editor/scripting/opcodes"; +} from "../../../../quest_editor/scripting/instructions"; +import { Opcode } from "../../../../quest_editor/scripting/opcodes"; import { prs_compress } from "../../compression/prs/compress"; import { prs_decompress } from "../../compression/prs/decompress"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; diff --git a/src/core/data_formats/parsing/quest/npc_types.ts b/src/core/data_formats/parsing/quest/npc_types.ts index 3435c6f7..466a9784 100644 --- a/src/core/data_formats/parsing/quest/npc_types.ts +++ b/src/core/data_formats/parsing/quest/npc_types.ts @@ -314,7 +314,7 @@ define_npc_type_data(NpcType.Scientist, "Scientist", "Scientist", "Scientist", u define_npc_type_data(NpcType.Nurse, "Nurse", "Nurse", "Nurse", undefined, false); define_npc_type_data(NpcType.Irene, "Irene", "Irene", "Irene", undefined, false); define_npc_type_data(NpcType.ItemShop, "Item Shop", "Item Shop", "Item Shop", undefined, false); -define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II);", "Nurse", "Nurse", 2, false); +define_npc_type_data(NpcType.Nurse2, "Nurse (Ep. II)", "Nurse", "Nurse", 2, false); // // Enemy NPCs @@ -450,17 +450,17 @@ define_npc_type_data(NpcType.DarkFalz, "Dark Falz", "Dark Falz", "Dark Falz", 1, define_npc_type_data( NpcType.Hildebear2, - "Hildebear (Ep. II);", + "Hildebear (Ep. II)", "Hildebear", "Hildelt", 2, true, NpcType.Hildeblue2, ); -define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II);", "Hildeblue", "Hildetorr", 2, true); +define_npc_type_data(NpcType.Hildeblue2, "Hildeblue (Ep. II)", "Hildeblue", "Hildetorr", 2, true); define_npc_type_data( NpcType.RagRappy2, - "Rag Rappy (Ep. II);", + "Rag Rappy (Ep. II)", "Rag Rappy", "El Rappy", 2, @@ -471,39 +471,39 @@ define_npc_type_data(NpcType.LoveRappy, "Love Rappy", "Love Rappy", "Love Rappy" define_npc_type_data(NpcType.StRappy, "St. Rappy", "St. Rappy", "St. Rappy", 2, true); define_npc_type_data(NpcType.HalloRappy, "Hallo Rappy", "Hallo Rappy", "Hallo Rappy", 2, true); define_npc_type_data(NpcType.EggRappy, "Egg Rappy", "Egg Rappy", "Egg Rappy", 2, true); -define_npc_type_data(NpcType.Monest2, "Monest (Ep. II);", "Monest", "Mothvist", 2, true); +define_npc_type_data(NpcType.Monest2, "Monest (Ep. II)", "Monest", "Mothvist", 2, true); define_npc_type_data(NpcType.Mothmant2, "Mothmant", "Mothmant", "Mothvert", 2, true); define_npc_type_data( NpcType.PoisonLily2, - "Poison Lily (Ep. II);", + "Poison Lily (Ep. II)", "Poison Lily", "Ob Lily", 2, true, NpcType.NarLily2, ); -define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II);", "Nar Lily", "Mil Lily", 2, true); +define_npc_type_data(NpcType.NarLily2, "Nar Lily (Ep. II)", "Nar Lily", "Mil Lily", 2, true); define_npc_type_data( NpcType.GrassAssassin2, - "Grass Assassin (Ep. II);", + "Grass Assassin (Ep. II)", "Grass Assassin", "Crimson Assassin", 2, true, ); -define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II);", "Dimenian", "Arlan", 2, true); +define_npc_type_data(NpcType.Dimenian2, "Dimenian (Ep. II)", "Dimenian", "Arlan", 2, true); define_npc_type_data( NpcType.LaDimenian2, - "La Dimenian (Ep. II);", + "La Dimenian (Ep. II)", "La Dimenian", "Merlan", 2, true, ); -define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II);", "So Dimenian", "Del-D", 2, true); +define_npc_type_data(NpcType.SoDimenian2, "So Dimenian (Ep. II)", "So Dimenian", "Del-D", 2, true); define_npc_type_data( NpcType.DarkBelra2, - "Dark Belra (Ep. II);", + "Dark Belra (Ep. II)", "Dark Belra", "Indi Belra", 2, @@ -515,7 +515,7 @@ define_npc_type_data(NpcType.BarbaRay, "Barba Ray", "Barba Ray", "Barba Ray", 2, define_npc_type_data( NpcType.SavageWolf2, - "Savage Wolf (Ep. II);", + "Savage Wolf (Ep. II)", "Savage Wolf", "Gulgus", 2, @@ -523,23 +523,23 @@ define_npc_type_data( ); define_npc_type_data( NpcType.BarbarousWolf2, - "Barbarous Wolf (Ep. II);", + "Barbarous Wolf (Ep. II)", "Barbarous Wolf", "Gulgus-Gue", 2, true, ); -define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II);", "Pan Arms", "Pan Arms", 2, true); -define_npc_type_data(NpcType.Migium2, "Migium (Ep. II);", "Migium", "Migium", 2, true); -define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II);", "Hidoom", "Hidoom", 2, true); -define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II);", "Dubchic", "Dubchich", 2, true); -define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II);", "Gilchic", "Gilchich", 2, true); -define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II);", "Garanz", "Baranz", 2, true); -define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II);", "Dubswitch", "Dubswitch", 2, true); -define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II);", "Delsaber", "Delsaber", 2, true); +define_npc_type_data(NpcType.PanArms2, "Pan Arms (Ep. II)", "Pan Arms", "Pan Arms", 2, true); +define_npc_type_data(NpcType.Migium2, "Migium (Ep. II)", "Migium", "Migium", 2, true); +define_npc_type_data(NpcType.Hidoom2, "Hidoom (Ep. II)", "Hidoom", "Hidoom", 2, true); +define_npc_type_data(NpcType.Dubchic2, "Dubchic (Ep. II)", "Dubchic", "Dubchich", 2, true); +define_npc_type_data(NpcType.Gilchic2, "Gilchic (Ep. II)", "Gilchic", "Gilchich", 2, true); +define_npc_type_data(NpcType.Garanz2, "Garanz (Ep. II)", "Garanz", "Baranz", 2, true); +define_npc_type_data(NpcType.Dubswitch2, "Dubswitch (Ep. II)", "Dubswitch", "Dubswitch", 2, true); +define_npc_type_data(NpcType.Delsaber2, "Delsaber (Ep. II)", "Delsaber", "Delsaber", 2, true); define_npc_type_data( NpcType.ChaosSorcerer2, - "Chaos Sorcerer (Ep. II);", + "Chaos Sorcerer (Ep. II)", "Chaos Sorcerer", "Gran Sorcerer", 2, diff --git a/src/core/gui/Button.ts b/src/core/gui/Button.ts index a44905aa..80153671 100644 --- a/src/core/gui/Button.ts +++ b/src/core/gui/Button.ts @@ -15,8 +15,8 @@ export class Button extends Control { this.element.append(create_element("span", { class: "core_Button_inner", text })); - this.enabled.observe(enabled => (this.element.disabled = !enabled)); + this.disposables(this.enabled.observe(({ value }) => (this.element.disabled = !value))); - this.element.onclick = (e: MouseEvent) => this._click.emit(e); + this.element.onclick = (e: MouseEvent) => this._click.emit({ value: e }); } } diff --git a/src/core/gui/CheckBox.ts b/src/core/gui/CheckBox.ts index c1bf0648..14e745a4 100644 --- a/src/core/gui/CheckBox.ts +++ b/src/core/gui/CheckBox.ts @@ -17,9 +17,9 @@ export class CheckBox extends LabelledControl { this.element.onchange = () => (this.checked.val = this.element.checked); this.disposables( - this.checked.observe(checked => (this.element.checked = checked)), + this.checked.observe(({ value }) => (this.element.checked = value)), - this.enabled.observe(enabled => (this.element.disabled = !enabled)), + this.enabled.observe(({ value }) => (this.element.disabled = !value)), ); this.checked.val = checked; diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index 1af86183..1ba306ef 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -38,14 +38,16 @@ export class FileButton extends Control { this.input, ); - this.enabled.observe(enabled => { - this.input.disabled = !enabled; + this.disposables( + this.enabled.observe(({ value }) => { + this.input.disabled = !value; - if (enabled) { - this.element.classList.remove("disabled"); - } else { - this.element.classList.add("disabled"); - } - }); + if (value) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } + }), + ); } } diff --git a/src/core/gui/Input.ts b/src/core/gui/Input.ts index c6ca167f..618a6fc4 100644 --- a/src/core/gui/Input.ts +++ b/src/core/gui/Input.ts @@ -35,12 +35,12 @@ export abstract class Input extends LabelledControl { this.element.append(this.input); this.disposables( - this.value.observe(value => this.set_input_value(value)), + this.value.observe(({ value }) => this.set_input_value(value)), - this.enabled.observe(enabled => { - this.input.disabled = !enabled; + this.enabled.observe(({ value }) => { + this.input.disabled = !value; - if (enabled) { + if (value) { this.element.classList.remove("disabled"); } else { this.element.classList.add("disabled"); @@ -71,7 +71,7 @@ export abstract class Input extends LabelledControl { if (is_any_property(value)) { input[attr] = cvt(value.val); - this.disposable(value.observe(v => (input[attr] = cvt(v)))); + this.disposable(value.observe(({ value }) => (input[attr] = cvt(value)))); } else { input[attr] = cvt(value); } diff --git a/src/core/gui/Label.ts b/src/core/gui/Label.ts index c26dbd78..8797b6c9 100644 --- a/src/core/gui/Label.ts +++ b/src/core/gui/Label.ts @@ -21,12 +21,12 @@ export class Label extends View { this.element.append(text); } else { this.element.append(text.val); - this.disposable(text.observe(text => (this.element.textContent = text))); + this.disposable(text.observe(({ value }) => (this.element.textContent = value))); } this.disposables( - this.enabled.observe(enabled => { - if (enabled) { + this.enabled.observe(({ value }) => { + if (value) { this.element.classList.remove("disabled"); } else { this.element.classList.add("disabled"); diff --git a/src/core/gui/LazyView.ts b/src/core/gui/LazyView.ts index 0d08e2b9..35730b48 100644 --- a/src/core/gui/LazyView.ts +++ b/src/core/gui/LazyView.ts @@ -15,8 +15,8 @@ export class LazyView extends ResizableView { this.visible.val = false; this.disposables( - this.visible.observe(visible => { - if (visible && !this.initialized) { + this.visible.observe(({ value }) => { + if (value && !this.initialized) { this.initialized = true; this.create_view().then(view => { diff --git a/src/core/gui/TextArea.ts b/src/core/gui/TextArea.ts index f419bd4e..c2f19510 100644 --- a/src/core/gui/TextArea.ts +++ b/src/core/gui/TextArea.ts @@ -39,7 +39,7 @@ export class TextArea extends LabelledControl { this.text_element.onchange = () => (this.value.val = this.text_element.value); - this.disposables(this.value.observe(value => (this.text_element.value = value))); + this.disposables(this.value.observe(({ value }) => (this.text_element.value = value))); this.element.append(this.text_element); } diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts index e2dd2ebc..99def902 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/View.ts @@ -21,7 +21,7 @@ export abstract class View implements Disposable { private disposer = new Disposer(); constructor() { - this.disposables(this.visible.observe(visible => (this.element.hidden = !visible))); + this.disposables(this.visible.observe(({ value }) => (this.element.hidden = !value))); } dispose(): void { diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 69a1452d..47cde39e 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -61,5 +61,5 @@ export function bind_hidden(element: HTMLElement, observable: Observable (element.hidden = v)); + return observable.observe(({ value }) => (element.hidden = value)); } diff --git a/src/core/observable/AbstractMinimalProperty.ts b/src/core/observable/AbstractMinimalProperty.ts index 9925ce80..9d15f3a8 100644 --- a/src/core/observable/AbstractMinimalProperty.ts +++ b/src/core/observable/AbstractMinimalProperty.ts @@ -1,4 +1,4 @@ -import { Property } from "./Property"; +import { Property, PropertyChangeEvent } from "./Property"; import { Disposable } from "./Disposable"; import Logger from "js-logger"; @@ -11,13 +11,22 @@ export abstract class AbstractMinimalProperty implements Property { abstract readonly val: T; - protected readonly observers: ((value: T) => void)[] = []; + abstract get_val(): T; - observe(observer: (value: T) => void): Disposable { + protected readonly observers: ((change: PropertyChangeEvent) => void)[] = []; + + observe( + observer: (change: PropertyChangeEvent) => void, + options: { call_now?: boolean } = {}, + ): Disposable { if (!this.observers.includes(observer)) { this.observers.push(observer); } + if (options.call_now) { + this.call_observer(observer, this.val); + } + return { dispose: () => { const index = this.observers.indexOf(observer); @@ -33,13 +42,17 @@ export abstract class AbstractMinimalProperty implements Property { abstract flat_map(f: (element: T) => Property): Property; - protected emit(): void { + protected emit(old_value: T): void { for (const observer of this.observers) { - try { - observer(this.val); - } catch (e) { - logger.error("Observer threw error.", e); - } + this.call_observer(observer, old_value); + } + } + + private call_observer(observer: (event: PropertyChangeEvent) => void, old_value: T): void { + try { + observer({ value: this.val, old_value }); + } catch (e) { + logger.error("Observer threw error.", e); } } } diff --git a/src/core/observable/DependentProperty.ts b/src/core/observable/DependentProperty.ts index 54dd806c..0a70e679 100644 --- a/src/core/observable/DependentProperty.ts +++ b/src/core/observable/DependentProperty.ts @@ -1,5 +1,5 @@ import { Disposable } from "./Disposable"; -import { Property } from "./Property"; +import { PropertyChangeEvent, Property } from "./Property"; import { Disposer } from "./Disposer"; import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; import { FlatMappedProperty } from "./FlatMappedProperty"; @@ -15,6 +15,10 @@ export class DependentProperty extends AbstractMinimalProperty implements private _val?: T; get val(): T { + return this.get_val(); + } + + get_val(): T { if (this.dependency_disposables.length) { return this._val as T; } else { @@ -28,7 +32,7 @@ export class DependentProperty extends AbstractMinimalProperty implements super(); } - observe(observer: (event: T) => void): Disposable { + observe(observer: (event: PropertyChangeEvent) => void): Disposable { const super_disposable = super.observe(observer); if (this.dependency_disposables.length === 0) { @@ -37,8 +41,9 @@ export class DependentProperty extends AbstractMinimalProperty implements this.dependency_disposables.add_all( ...this.dependencies.map(dependency => dependency.observe(() => { + const old_value = this._val!; this._val = this.f(); - this.emit(); + this.emit(old_value); }), ), ); @@ -49,7 +54,7 @@ export class DependentProperty extends AbstractMinimalProperty implements super_disposable.dispose(); if (this.observers.length === 0) { - this.dependency_disposables.dispose(); + this.dependency_disposables.dispose_all(); } }, }; diff --git a/src/core/observable/Disposable.ts b/src/core/observable/Disposable.ts index 3cbd9d2a..3374b70b 100644 --- a/src/core/observable/Disposable.ts +++ b/src/core/observable/Disposable.ts @@ -1,3 +1,10 @@ +/** + * Objects implementing this interface should be disposed when they're not used anymore. + * This is to avoid e.g. memory leaks. + */ export interface Disposable { + /** + * Releases any held resources. + */ dispose(): void; } diff --git a/src/core/observable/Disposer.test.ts b/src/core/observable/Disposer.test.ts new file mode 100644 index 00000000..1e638241 --- /dev/null +++ b/src/core/observable/Disposer.test.ts @@ -0,0 +1,51 @@ +import { Disposer } from "./Disposer"; +import { Disposable } from "./Disposable"; + +test("calling add or add_all should increase length correctly", () => { + const disposer = new Disposer(); + expect(disposer.length).toBe(0); + + disposer.add(dummy()); + expect(disposer.length).toBe(1); + + disposer.add_all(dummy(), dummy()); + expect(disposer.length).toBe(3); + + disposer.add(dummy()); + expect(disposer.length).toBe(4); + + disposer.add_all(dummy(), dummy()); + expect(disposer.length).toBe(6); +}); + +test("length should be 0 after calling dispose", () => { + const disposer = new Disposer(); + disposer.add_all(dummy(), dummy(), dummy()); + expect(disposer.length).toBe(3); + + disposer.dispose(); + expect(disposer.length).toBe(0); +}); + +test("contained disposables should be disposed when calling dispose", () => { + let dispose_calls = 0; + + function disposable(): Disposable { + return { + dispose(): void { + dispose_calls++; + }, + }; + } + + const disposer = new Disposer(); + disposer.add_all(disposable(), disposable(), disposable()); + expect(dispose_calls).toBe(0); + + disposer.dispose(); + expect(dispose_calls).toBe(3); +}); + +function dummy(): Disposable { + return { dispose(): void {} }; +} diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index 5ae1ae1d..a78c286f 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -3,24 +3,42 @@ import Logger = require("js-logger"); const logger = Logger.get("core/observable/Disposer"); +/** + * Container for disposables. + */ export class Disposer implements Disposable { private readonly disposables: Disposable[] = []; + private disposed = false; + /** + * The amount of disposables contained in this disposer. + */ get length(): number { return this.disposables.length; } + /** + * Add a single disposable and return the given disposable. + */ add(disposable: T): T { + this.check_not_disposed(); this.disposables.push(disposable); return disposable; } + /** + * Add 0 or more disposables. + */ add_all(...disposable: Disposable[]): this { + this.check_not_disposed(); this.disposables.push(...disposable); return this; } - dispose(): void { + /** + * Disposes all held disposables. + */ + dispose_all(): void { for (const disposable of this.disposables.splice(0, this.disposables.length)) { try { disposable.dispose(); @@ -29,4 +47,18 @@ export class Disposer implements Disposable { } } } + + /** + * Disposes all held disposables. + */ + dispose(): void { + this.dispose_all(); + this.disposed = true; + } + + private check_not_disposed(): void { + if (this.disposed) { + throw new Error("This disposer has been disposed."); + } + } } diff --git a/src/core/observable/Emitter.ts b/src/core/observable/Emitter.ts index 6306e3f6..18a937ef 100644 --- a/src/core/observable/Emitter.ts +++ b/src/core/observable/Emitter.ts @@ -1,5 +1,5 @@ -import { Observable } from "./Observable"; +import { ChangeEvent, Observable } from "./Observable"; -export interface Emitter extends Observable { - emit(event: E): void; +export interface Emitter extends Observable { + emit(event: ChangeEvent): void; } diff --git a/src/core/observable/FlatMappedProperty.ts b/src/core/observable/FlatMappedProperty.ts index 61a30c39..60e03ef3 100644 --- a/src/core/observable/FlatMappedProperty.ts +++ b/src/core/observable/FlatMappedProperty.ts @@ -1,4 +1,4 @@ -import { Property } from "./Property"; +import { PropertyChangeEvent, Property } from "./Property"; import { Disposable } from "./Disposable"; import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; import { DependentProperty } from "./DependentProperty"; @@ -12,6 +12,10 @@ export class FlatMappedProperty extends AbstractMinimalProperty impleme readonly is_property = true; get val(): U { + return this.get_val(); + } + + get_val(): U { return this.computed_property ? this.computed_property.val : this.f(this.dependency.val).val; @@ -25,13 +29,14 @@ export class FlatMappedProperty extends AbstractMinimalProperty impleme super(); } - observe(observer: (value: U) => void): Disposable { + observe(observer: (event: PropertyChangeEvent) => void): Disposable { const super_disposable = super.observe(observer); if (this.dependency_disposable == undefined) { this.dependency_disposable = this.dependency.observe(() => { + const old_value = this.val; this.compute_and_observe(); - this.emit(); + this.emit(old_value); }); this.compute_and_observe(); @@ -62,9 +67,15 @@ export class FlatMappedProperty extends AbstractMinimalProperty impleme private compute_and_observe(): void { if (this.computed_disposable) this.computed_disposable.dispose(); + this.computed_property = this.f(this.dependency.val); + + let old_value = this.computed_property.val; + this.computed_disposable = this.computed_property.observe(() => { - this.emit(); + const ov = old_value; + old_value = this.val; + this.emit(ov); }); } } diff --git a/src/core/observable/Observable.ts b/src/core/observable/Observable.ts index 9b89004a..a9fb3644 100644 --- a/src/core/observable/Observable.ts +++ b/src/core/observable/Observable.ts @@ -1,5 +1,9 @@ import { Disposable } from "./Disposable"; -export interface Observable { - observe(observer: (event: E) => void): Disposable; +export interface ChangeEvent { + value: T; +} + +export interface Observable { + observe(observer: (event: ChangeEvent) => void): Disposable; } diff --git a/src/core/observable/Property.ts b/src/core/observable/Property.ts index 01310d8b..68911439 100644 --- a/src/core/observable/Property.ts +++ b/src/core/observable/Property.ts @@ -1,10 +1,22 @@ -import { Observable } from "./Observable"; +import { ChangeEvent, Observable } from "./Observable"; +import { Disposable } from "./Disposable"; + +export interface PropertyChangeEvent extends ChangeEvent { + old_value: T; +} export interface Property extends Observable { readonly is_property: true; readonly val: T; + get_val(): T; + + observe( + observer: (event: PropertyChangeEvent) => void, + options?: { call_now?: boolean }, + ): Disposable; + map(f: (element: T) => U): Property; flat_map(f: (element: T) => Property): Property; diff --git a/src/core/observable/SimpleEmitter.ts b/src/core/observable/SimpleEmitter.ts index 2c584447..df854f53 100644 --- a/src/core/observable/SimpleEmitter.ts +++ b/src/core/observable/SimpleEmitter.ts @@ -1,12 +1,14 @@ import { Disposable } from "./Disposable"; import Logger from "js-logger"; +import { Emitter } from "./Emitter"; +import { ChangeEvent } from "./Observable"; const logger = Logger.get("core/observable/SimpleEmitter"); -export class SimpleEmitter { - protected readonly observers: ((event: E) => void)[] = []; +export class SimpleEmitter implements Emitter { + protected readonly observers: ((event: ChangeEvent) => void)[] = []; - emit(event: E): void { + emit(event: ChangeEvent): void { for (const observer of this.observers) { try { observer(event); @@ -16,7 +18,7 @@ export class SimpleEmitter { } } - observe(observer: (event: E) => void): Disposable { + observe(observer: (event: ChangeEvent) => void): Disposable { if (!this.observers.includes(observer)) { this.observers.push(observer); } diff --git a/src/core/observable/SimpleProperty.ts b/src/core/observable/SimpleProperty.ts index d60bf279..40249ff8 100644 --- a/src/core/observable/SimpleProperty.ts +++ b/src/core/observable/SimpleProperty.ts @@ -5,20 +5,30 @@ import { is_property } from "./Property"; import { AbstractProperty } from "./AbstractProperty"; export class SimpleProperty extends AbstractProperty implements WritableProperty { - readonly is_writable_property = true; - constructor(private _val: T) { super(); } get val(): T { + return this.get_val(); + } + + set val(value: T) { + this.set_val(value); + } + + get_val(): T { return this._val; } - set val(val: T) { + set_val(val: T, options: { silent?: boolean } = {}): void { if (val !== this._val) { + const old_value = this._val; this._val = val; - this.emit(); + + if (!options.silent) { + this.emit(old_value); + } } } @@ -26,17 +36,17 @@ export class SimpleProperty extends AbstractProperty implements WritablePr this.val = f(this.val); } - bind(observable: Observable): Disposable { + bind_to(observable: Observable): Disposable { if (is_property(observable)) { this.val = observable.val; } - return observable.observe(v => (this.val = v)); + return observable.observe(event => (this.val = event.value)); } bind_bi(property: WritableProperty): Disposable { - const bind_1 = this.bind(property); - const bind_2 = property.bind(this); + const bind_1 = this.bind_to(property); + const bind_2 = property.bind_to(this); return { dispose(): void { bind_1.dispose(); diff --git a/src/core/observable/SimpleWritableArrayProperty.ts b/src/core/observable/SimpleWritableArrayProperty.ts index 27dd9450..2f47a2b7 100644 --- a/src/core/observable/SimpleWritableArrayProperty.ts +++ b/src/core/observable/SimpleWritableArrayProperty.ts @@ -10,23 +10,35 @@ export class SimpleWritableArrayProperty extends AbstractProperty implements WritableArrayProperty { readonly is_property = true; - readonly is_writable_property = true; - private readonly _length = property(0); readonly length = this._length; private readonly values: T[]; get val(): T[] { + return this.get_val(); + } + + set val(values: T[]) { + this.set_val(values); + } + + get_val(): T[] { return this.values; } + set_val(values: T[]): T[] { + const replaced_values = this.values.splice(0, this.values.length, ...values); + this.emit(this.values); + return replaced_values; + } + constructor(...values: T[]) { super(); this.values = values; } - bind(observable: Observable): Disposable { + bind_to(observable: Observable): Disposable { /* TODO */ throw new Error("not implemented"); } @@ -44,12 +56,12 @@ export class SimpleWritableArrayProperty extends AbstractProperty set(index: number, value: T): void { this.values[index] = value; - this.emit(); + this.emit(this.values); } clear(): void { this.values.splice(0, this.values.length); - this.emit(); + this.emit(this.values); } splice(index: number, delete_count?: number): T[]; @@ -63,7 +75,7 @@ export class SimpleWritableArrayProperty extends AbstractProperty ret = this.values.splice(index, delete_count, ...items); } - this.emit(); + this.emit(this.values); return ret; } diff --git a/src/core/observable/WritableProperty.ts b/src/core/observable/WritableProperty.ts index a8d9a4b5..cba10566 100644 --- a/src/core/observable/WritableProperty.ts +++ b/src/core/observable/WritableProperty.ts @@ -3,10 +3,10 @@ import { Observable } from "./Observable"; import { Disposable } from "./Disposable"; export interface WritableProperty extends Property { - readonly is_writable_property: true; - val: T; + set_val(value: T, options?: { silent?: boolean }): void; + update(f: (value: T) => T): void; /** @@ -14,13 +14,7 @@ export interface WritableProperty extends Property { * * @param observable the observable who's events will be propagated to this property. */ - bind(observable: Observable): Disposable; + bind_to(observable: Observable): Disposable; bind_bi(property: WritableProperty): Disposable; } - -export function is_writable_property( - observable: Observable, -): observable is WritableProperty { - return (observable as any).is_writable_property; -} diff --git a/src/old/core/primitive_conversion.ts b/src/core/primitive_conversion.ts similarity index 100% rename from src/old/core/primitive_conversion.ts rename to src/core/primitive_conversion.ts diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 27709740..83ece4f2 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -24,7 +24,7 @@ CameraControls.install({ }); export abstract class Renderer implements Disposable { - protected _debug = false; + private _debug = false; get debug(): boolean { return this._debug; diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index 3397ea51..5caf84b0 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -18,7 +18,7 @@ const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v] class GuiStore implements Disposable { readonly tool: WritableProperty = property(GuiTool.Viewer); - private hash_disposer = this.tool.observe(tool => { + private hash_disposer = this.tool.observe(({ value: tool }) => { window.location.hash = `#/${gui_tool_to_string(tool)}`; }); diff --git a/src/core/undo/Action.ts b/src/core/undo/Action.ts index 910ab9a5..5b429ffe 100644 --- a/src/core/undo/Action.ts +++ b/src/core/undo/Action.ts @@ -1,7 +1,5 @@ -export class Action { - constructor( - readonly description: string, - readonly undo: () => void, - readonly redo: () => void, - ) {} +export interface Action { + readonly description: string; + readonly undo: () => void; + readonly redo: () => void; } diff --git a/src/core/undo/SimpleUndo.ts b/src/core/undo/SimpleUndo.ts index 7cca7dd5..a1c29e54 100644 --- a/src/core/undo/SimpleUndo.ts +++ b/src/core/undo/SimpleUndo.ts @@ -9,12 +9,10 @@ import { undo_manager } from "./UndoManager"; * Simply contains a single action. `can_undo` and `can_redo` must be managed manually. */ export class SimpleUndo implements Undo { - private readonly _action: Action; - readonly action: Property; + private readonly action: Action; constructor(description: string, undo: () => void, redo: () => void) { - this._action = new Action(description, undo, redo); - this.action = property(this._action); + this.action = { description, undo, redo }; } make_current(): void { @@ -32,16 +30,16 @@ export class SimpleUndo implements Undo { readonly can_redo = property(false); readonly first_undo: Property = this.can_undo.map(can_undo => - can_undo ? this._action : undefined, + can_undo ? this.action : undefined, ); readonly first_redo: Property = this.can_redo.map(can_redo => - can_redo ? this._action : undefined, + can_redo ? this.action : undefined, ); undo(): boolean { if (this.can_undo) { - this._action.undo(); + this.action.undo(); return true; } else { return false; @@ -50,7 +48,7 @@ export class SimpleUndo implements Undo { redo(): boolean { if (this.can_redo) { - this._action.redo(); + this.action.redo(); return true; } else { return false; diff --git a/src/core/undo/index.test.ts b/src/core/undo/UndoStack.test.ts similarity index 69% rename from src/core/undo/index.test.ts rename to src/core/undo/UndoStack.test.ts index c36cfa5b..c2caf9e0 100644 --- a/src/core/undo/index.test.ts +++ b/src/core/undo/UndoStack.test.ts @@ -1,4 +1,3 @@ -import { Action } from "./Action"; import { UndoStack } from "./UndoStack"; test("simple properties and invariants", () => { @@ -7,9 +6,9 @@ test("simple properties and invariants", () => { expect(stack.can_undo.val).toBe(false); expect(stack.can_redo.val).toBe(false); - stack.push(new Action("", () => {}, () => {})); - stack.push(new Action("", () => {}, () => {})); - stack.push(new Action("", () => {}, () => {})); + stack.push({ description: "", undo: () => {}, redo: () => {} }); + stack.push({ description: "", undo: () => {}, redo: () => {} }); + stack.push({ description: "", undo: () => {}, redo: () => {} }); expect(stack.can_undo.val).toBe(true); expect(stack.can_redo.val).toBe(false); @@ -32,8 +31,8 @@ test("undo", () => { // Pretend value started and 3 and we've set it to 7 and then 13. let value = 13; - stack.push(new Action("X", () => (value = 3), () => (value = 7))); - stack.push(new Action("Y", () => (value = 7), () => (value = 13))); + stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) }); + stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) }); expect(stack.undo()).toBe(true); expect(value).toBe(7); @@ -51,8 +50,8 @@ test("redo", () => { // Pretend value started and 3 and we've set it to 7 and then 13. let value = 13; - stack.push(new Action("X", () => (value = 3), () => (value = 7))); - stack.push(new Action("Y", () => (value = 7), () => (value = 13))); + stack.push({ description: "X", undo: () => (value = 3), redo: () => (value = 7) }); + stack.push({ description: "Y", undo: () => (value = 7), redo: () => (value = 13) }); stack.undo(); stack.undo(); diff --git a/src/core/undo/UndoStack.ts b/src/core/undo/UndoStack.ts index 22c8927a..4be23f04 100644 --- a/src/core/undo/UndoStack.ts +++ b/src/core/undo/UndoStack.ts @@ -4,6 +4,9 @@ import { Action } from "./Action"; import { array_property, map, property } from "../observable"; import { NOOP_UNDO } from "./noop_undo"; import { undo_manager } from "./UndoManager"; +import Logger = require("js-logger"); + +const logger = Logger.get("core/undo/UndoStack"); /** * Full-fledged linear undo/redo implementation. @@ -16,16 +19,6 @@ export class UndoStack implements Undo { */ private readonly index = property(0); - make_current(): void { - undo_manager.current.val = this; - } - - ensure_not_current(): void { - if (undo_manager.current.val === this) { - undo_manager.current.val = NOOP_UNDO; - } - } - readonly can_undo = this.index.map(index => index > 0); readonly can_redo = map((stack, index) => index < stack.length, this.stack, this.index); @@ -38,13 +31,25 @@ export class UndoStack implements Undo { return can_redo ? this.stack.get(this.index.val) : undefined; }); - push_action(description: string, undo: () => void, redo: () => void): void { - this.push(new Action(description, undo, redo)); + private undoing_or_redoing = false; + + make_current(): void { + undo_manager.current.val = this; } - push(action: Action): void { - this.stack.splice(this.index.val, this.stack.length.val - this.index.val, action); - this.index.update(i => i + 1); + ensure_not_current(): void { + if (undo_manager.current.val === this) { + undo_manager.current.val = NOOP_UNDO; + } + } + + push(action: Action): Action { + if (!this.undoing_or_redoing) { + this.stack.splice(this.index.val, Infinity, action); + this.index.update(i => i + 1); + } + + return action; } /** @@ -56,9 +61,17 @@ export class UndoStack implements Undo { } undo(): boolean { - if (this.can_undo) { - this.index.update(i => i - 1); - this.stack.get(this.index.val).undo(); + if (this.can_undo.val && !this.undoing_or_redoing) { + try { + this.undoing_or_redoing = true; + this.index.update(i => i - 1); + this.stack.get(this.index.val).undo(); + } catch (e) { + logger.warn("Error while undoing action.", e); + } finally { + this.undoing_or_redoing = false; + } + return true; } else { return false; @@ -66,9 +79,17 @@ export class UndoStack implements Undo { } redo(): boolean { - if (this.can_redo) { - this.stack.get(this.index.val).redo(); - this.index.update(i => i + 1); + if (this.can_redo.val && !this.undoing_or_redoing) { + try { + this.undoing_or_redoing = true; + this.stack.get(this.index.val).redo(); + this.index.update(i => i + 1); + } catch (e) { + logger.warn("Error while redoing action.", e); + } finally { + this.undoing_or_redoing = false; + } + return true; } else { return false; diff --git a/src/old/core/undo.test.ts b/src/old/core/undo.test.ts deleted file mode 100644 index 8c5f88d9..00000000 --- a/src/old/core/undo.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { UndoStack, Action } from "./undo"; - -test("simple properties and invariants", () => { - const stack = new UndoStack(); - - expect(stack.can_undo).toBe(false); - expect(stack.can_redo).toBe(false); - - stack.push(new Action("", () => {}, () => {})); - stack.push(new Action("", () => {}, () => {})); - stack.push(new Action("", () => {}, () => {})); - - expect(stack.can_undo).toBe(true); - expect(stack.can_redo).toBe(false); - - stack.undo(); - - expect(stack.can_undo).toBe(true); - expect(stack.can_redo).toBe(true); - - stack.undo(); - stack.undo(); - - expect(stack.can_undo).toBe(false); - expect(stack.can_redo).toBe(true); -}); - -test("undo", () => { - const stack = new UndoStack(); - - // Pretend value started and 3 and we've set it to 7 and then 13. - let value = 13; - - stack.push(new Action("X", () => (value = 3), () => (value = 7))); - stack.push(new Action("Y", () => (value = 7), () => (value = 13))); - - expect(stack.undo()).toBe(true); - expect(value).toBe(7); - - expect(stack.undo()).toBe(true); - expect(value).toBe(3); - - expect(stack.undo()).toBe(false); - expect(value).toBe(3); -}); - -test("redo", () => { - const stack = new UndoStack(); - - // Pretend value started and 3 and we've set it to 7 and then 13. - let value = 13; - - stack.push(new Action("X", () => (value = 3), () => (value = 7))); - stack.push(new Action("Y", () => (value = 7), () => (value = 13))); - - stack.undo(); - stack.undo(); - - expect(value).toBe(3); - - expect(stack.redo()).toBe(true); - expect(value).toBe(7); - - expect(stack.redo()).toBe(true); - expect(value).toBe(13); - - expect(stack.redo()).toBe(false); - expect(value).toBe(13); -}); diff --git a/src/old/quest_editor/domain/ObservableAreaVariant.ts b/src/old/quest_editor/domain/ObservableAreaVariant.ts deleted file mode 100644 index 1366346f..00000000 --- a/src/old/quest_editor/domain/ObservableAreaVariant.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ObservableArea } from "./ObservableArea"; -import { IObservableArray, observable } from "mobx"; -import { Section } from "./Section"; - -export class ObservableAreaVariant { - readonly id: number; - readonly area: ObservableArea; - @observable.shallow readonly sections: IObservableArray
= observable.array(); - - constructor(id: number, area: ObservableArea) { - if (!Number.isInteger(id) || id < 0) - throw new Error(`Expected id to be a non-negative integer, got ${id}.`); - - this.id = id; - this.area = area; - } -} diff --git a/src/old/quest_editor/domain/ObservableQuest.ts b/src/old/quest_editor/domain/ObservableQuest.ts deleted file mode 100644 index 6b896c73..00000000 --- a/src/old/quest_editor/domain/ObservableQuest.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { action, computed, observable } from "mobx"; -import { check_episode, Episode } from "../../../core/data_formats/parsing/quest/Episode"; -import { ObservableAreaVariant } from "./ObservableAreaVariant"; -import { area_store } from "../stores/AreaStore"; -import { DatUnknown } from "../../../core/data_formats/parsing/quest/dat"; -import { Segment } from "../scripting/instructions"; -import Logger from "js-logger"; -import { ObservableQuestNpc, ObservableQuestObject } from "./observable_quest_entities"; - -const logger = Logger.get("domain/ObservableQuest"); - -export class ObservableQuest { - @observable private _id!: number; - - get id(): number { - return this._id; - } - - @action - set_id(id: number): void { - if (!Number.isInteger(id) || id < 0 || id > 4294967295) - throw new Error("id must be an integer greater than 0 and less than 4294967295."); - this._id = id; - } - - @observable private _language!: number; - - get language(): number { - return this._language; - } - - @action - set_language(language: number): void { - if (!Number.isInteger(language)) throw new Error("language must be an integer."); - this._language = language; - } - - @observable private _name!: string; - - get name(): string { - return this._name; - } - - @action - set_name(name: string): void { - if (name.length > 32) throw new Error("name can't be longer than 32 characters."); - this._name = name; - } - - @observable private _short_description!: string; - - get short_description(): string { - return this._short_description; - } - - @action - set_short_description(short_description: string): void { - if (short_description.length > 128) - throw new Error("short_description can't be longer than 128 characters."); - this._short_description = short_description; - } - - @observable private _long_description!: string; - - get long_description(): string { - return this._long_description; - } - - @action - set_long_description(long_description: string): void { - if (long_description.length > 288) - throw new Error("long_description can't be longer than 288 characters."); - this._long_description = long_description; - } - - readonly episode: Episode; - - @observable readonly objects: ObservableQuestObject[]; - @observable readonly npcs: ObservableQuestNpc[]; - - /** - * Map of area IDs to entity counts. - */ - @computed get entities_per_area(): Map { - const map = new Map(); - - for (const npc of this.npcs) { - map.set(npc.area_id, (map.get(npc.area_id) || 0) + 1); - } - - for (const obj of this.objects) { - map.set(obj.area_id, (map.get(obj.area_id) || 0) + 1); - } - - return map; - } - - @observable.ref private _map_designations!: Map; - - /** - * Map of area IDs to area variant IDs. One designation per area. - */ - get map_designations(): Map { - return this._map_designations; - } - - set_map_designations(map_designations: Map): void { - this._map_designations = map_designations; - } - - /** - * One variant per area. - */ - @computed get area_variants(): ObservableAreaVariant[] { - const variants = new Map(); - - for (const area_id of this.entities_per_area.keys()) { - try { - variants.set(area_id, area_store.get_variant(this.episode, area_id, 0)); - } catch (e) { - logger.warn(e); - } - } - - for (const [area_id, variant_id] of this._map_designations) { - try { - variants.set(area_id, area_store.get_variant(this.episode, area_id, variant_id)); - } catch (e) { - logger.warn(e); - } - } - - return [...variants.values()]; - } - - /** - * (Partial) raw DAT data that can't be parsed yet by Phantasmal. - */ - readonly dat_unknowns: DatUnknown[]; - readonly object_code: Segment[]; - readonly shop_items: number[]; - - constructor( - id: number, - language: number, - name: string, - short_description: string, - long_description: string, - episode: Episode, - map_designations: Map, - objects: ObservableQuestObject[], - npcs: ObservableQuestNpc[], - dat_unknowns: DatUnknown[], - object_code: Segment[], - shop_items: number[], - ) { - check_episode(episode); - if (!map_designations) throw new Error("map_designations is required."); - if (!Array.isArray(objects)) throw new Error("objs is required."); - if (!Array.isArray(npcs)) throw new Error("npcs is required."); - if (!dat_unknowns) throw new Error("dat_unknowns is required."); - if (!object_code) throw new Error("object_code is required."); - if (!shop_items) throw new Error("shop_items is required."); - - this.set_id(id); - this.set_language(language); - this.set_name(name); - this.set_short_description(short_description); - this.set_long_description(long_description); - this.episode = episode; - this.set_map_designations(map_designations); - this.objects = objects; - this.npcs = npcs; - this.dat_unknowns = dat_unknowns; - this.object_code = object_code; - this.shop_items = shop_items; - } -} diff --git a/src/old/quest_editor/domain/observable_quest_entities.ts b/src/old/quest_editor/domain/observable_quest_entities.ts index c2cd789b..73aa7f87 100644 --- a/src/old/quest_editor/domain/observable_quest_entities.ts +++ b/src/old/quest_editor/domain/observable_quest_entities.ts @@ -2,13 +2,13 @@ import { ObjectType } from "../../../core/data_formats/parsing/quest/object_type import { action, computed, observable } from "mobx"; import { Vec3 } from "../../../core/data_formats/vector"; import { EntityType } from "../../../core/data_formats/parsing/quest/entities"; -import { Section } from "./Section"; +import { SectionModel } from "../../../quest_editor/model/SectionModel"; import { NpcType } from "../../../core/data_formats/parsing/quest/npc_types"; /** - * Abstract class from which ObservableQuestNpc and ObservableQuestObject derive. + * Abstract class from which ObservableQuestNpc and QuestObjectModel derive. */ -export abstract class ObservableQuestEntity { +export abstract class QuestEntityModel { readonly type: Type; @observable area_id: number; @@ -19,7 +19,7 @@ export abstract class ObservableQuestEntity { +export class ObservableQuestObject extends QuestEntityModel { readonly id: number; readonly group_id: number; @@ -145,7 +145,7 @@ export class ObservableQuestObject extends ObservableQuestEntity { } } -export class ObservableQuestNpc extends ObservableQuestEntity { +export class ObservableQuestNpc extends QuestEntityModel { readonly pso_type_id: number; readonly npc_id: number; readonly script_label: number; diff --git a/src/old/quest_editor/stores/QuestEditorStore.ts b/src/old/quest_editor/stores/QuestEditorStore.ts index a70febce..f5c85f7b 100644 --- a/src/old/quest_editor/stores/QuestEditorStore.ts +++ b/src/old/quest_editor/stores/QuestEditorStore.ts @@ -7,14 +7,14 @@ import { Vec3 } from "../../../core/data_formats/vector"; import { read_file } from "../../../core/read_file"; import { SimpleUndo, UndoStack } from "../../core/undo"; import { area_store } from "./AreaStore"; -import { create_new_quest } from "./quest_creation"; +import { create_new_quest } from "../../../quest_editor/stores/quest_creation"; import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { entity_data } from "../../../core/data_formats/parsing/quest/entities"; -import { ObservableQuest } from "../domain/ObservableQuest"; -import { ObservableArea } from "../domain/ObservableArea"; -import { Section } from "../domain/Section"; +import { ObservableQuest } from "../domain/QuestModel"; +import { AreaModel } from "../../../quest_editor/model/AreaModel"; +import { SectionModel } from "../../../quest_editor/model/SectionModel"; import { - ObservableQuestEntity, + QuestEntityModel, ObservableQuestNpc, ObservableQuestObject, } from "../domain/observable_quest_entities"; @@ -29,9 +29,9 @@ class QuestEditorStore { @observable current_quest_filename?: string; @observable current_quest?: ObservableQuest; - @observable current_area?: ObservableArea; + @observable current_area?: AreaModel; - @observable selected_entity?: ObservableQuestEntity; + @observable selected_entity?: QuestEntityModel; @observable save_dialog_filename?: string; @observable save_dialog_open: boolean = false; @@ -58,7 +58,7 @@ class QuestEditorStore { }; @action - set_selected_entity = (entity?: ObservableQuestEntity) => { + set_selected_entity = (entity?: QuestEntityModel) => { if (entity) { this.set_current_area_id(entity.area_id); } @@ -299,7 +299,7 @@ class QuestEditorStore { @action push_entity_move_action = ( - entity: ObservableQuestEntity, + entity: QuestEntityModel, old_position: Vec3, new_position: Vec3, ) => { @@ -368,7 +368,7 @@ class QuestEditorStore { } }); - private set_section_on_quest_entity = (entity: ObservableQuestEntity, sections: Section[]) => { + private set_section_on_quest_entity = (entity: QuestEntityModel, sections: SectionModel[]) => { const section = sections.find(s => s.id === entity.section_id); if (section) { diff --git a/src/old/quest_editor/ui/AssemblyEditorComponent.tsx b/src/old/quest_editor/ui/AssemblyEditorComponent.tsx index 4feb4b30..408ae932 100644 --- a/src/old/quest_editor/ui/AssemblyEditorComponent.tsx +++ b/src/old/quest_editor/ui/AssemblyEditorComponent.tsx @@ -2,7 +2,7 @@ import { autorun } from "mobx"; import { editor, languages, MarkerSeverity, MarkerTag, Position } from "monaco-editor"; import React, { Component, createRef, ReactNode } from "react"; import { AutoSizer } from "react-virtualized"; -import { AssemblyAnalyser } from "../scripting/AssemblyAnalyser"; +import { AssemblyAnalyser } from "../../../quest_editor/scripting/AssemblyAnalyser"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { Action } from "../../core/undo"; import styles from "./AssemblyEditorComponent.css"; diff --git a/src/old/quest_editor/ui/EntityInfoComponent.tsx b/src/old/quest_editor/ui/EntityInfoComponent.tsx index 9733b4eb..b8de159d 100644 --- a/src/old/quest_editor/ui/EntityInfoComponent.tsx +++ b/src/old/quest_editor/ui/EntityInfoComponent.tsx @@ -7,7 +7,7 @@ import { quest_editor_store } from "../stores/QuestEditorStore"; import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent"; import styles from "./EntityInfoComponent.css"; import { entity_data, entity_type_to_string } from "../../../core/data_formats/parsing/quest/entities"; -import { ObservableQuestEntity, ObservableQuestNpc } from "../domain/observable_quest_entities"; +import { QuestEntityModel, ObservableQuestNpc } from "../domain/observable_quest_entities"; @observer export class EntityInfoComponent extends Component { @@ -57,7 +57,7 @@ export class EntityInfoComponent extends Component { } type CoordProps = { - entity: ObservableQuestEntity; + entity: QuestEntityModel; position_type: "position" | "world_position"; coord: "x" | "y" | "z"; }; diff --git a/src/old/quest_editor/ui/QuestInfoComponent.css b/src/old/quest_editor/ui/QuestInfoComponent.css deleted file mode 100644 index 775df906..00000000 --- a/src/old/quest_editor/ui/QuestInfoComponent.css +++ /dev/null @@ -1,17 +0,0 @@ -.main { - height: 100%; - width: 100%; - padding: 2px 10px 10px 10px; - display: flex; - flex-direction: column; - overflow: auto; -} - -.main table { - border-collapse: collapse; - width: 100%; -} - -.main textarea { - font-family: 'Courier New', Courier, monospace -} diff --git a/src/old/quest_editor/ui/QuestInfoComponent.tsx b/src/old/quest_editor/ui/QuestInfoComponent.tsx deleted file mode 100644 index 7fe3739b..00000000 --- a/src/old/quest_editor/ui/QuestInfoComponent.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { observer } from "mobx-react"; -import React, { ChangeEvent, Component, ReactNode } from "react"; -import { quest_editor_store } from "../stores/QuestEditorStore"; -import { DisabledTextComponent } from "../../core/ui/DisabledTextComponent"; -import styles from "./QuestInfoComponent.css"; -import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; -import { NumberInput } from "../../core/ui/NumberInput"; -import { TextInput } from "../../core/ui/TextInput"; -import { TextArea } from "../../core/ui/TextArea"; - -@observer -export class QuestInfoComponent extends Component { - render(): ReactNode { - const quest = quest_editor_store.current_quest; - let body: ReactNode; - - if (quest) { - const episode = - quest.episode === Episode.IV ? "IV" : quest.episode === Episode.II ? "II" : "I"; - - body = ( - - - - - - - - - - - - - - - - - - -
Episode:{episode}
ID: - -
Name: - -
Short description:
-