From 03dc60cec9e5c247247fb1d4a095ddc727461e66 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Mon, 26 Aug 2019 15:42:12 +0200 Subject: [PATCH] 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:
-