From eed7b5d68e16e0730745c07da958789dceb9f132 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 29 May 2019 17:04:06 +0200 Subject: [PATCH] All state changes now happen via mobx actions. Fixed bug that allowed users to save .qst files with names that are too long (and thus corrupting the file). --- src/actions.ts | 114 ------------------ src/actions/appState.ts | 66 ++++++++++ src/actions/loadFile.ts | 100 +++++++++++++++ src/actions/visibleQuestEntities.ts | 12 ++ src/data/parsing/qst.ts | 12 +- src/data/parsing/quest.ts | 16 ++- src/index.tsx | 5 + src/rendering/Renderer.ts | 47 +++----- src/rendering/entities.test.ts | 43 +++---- src/rendering/entities.ts | 33 ++--- src/stores/AppStateStore.ts | 12 ++ src/{store.ts => stores/AreaStore.ts} | 13 +- src/ui/ApplicationComponent.tsx | 87 ++++++------- ...a3DComponent.tsx => RendererComponent.tsx} | 20 +-- 14 files changed, 306 insertions(+), 274 deletions(-) delete mode 100644 src/actions.ts create mode 100644 src/actions/appState.ts create mode 100644 src/actions/loadFile.ts create mode 100644 src/actions/visibleQuestEntities.ts create mode 100644 src/stores/AppStateStore.ts rename src/{store.ts => stores/AreaStore.ts} (89%) rename src/ui/{Area3DComponent.tsx => RendererComponent.tsx} (67%) diff --git a/src/actions.ts b/src/actions.ts deleted file mode 100644 index 18f4cf47..00000000 --- a/src/actions.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ArrayBufferCursor } from './data/ArrayBufferCursor'; -import { applicationState } from './store'; -import { parseQuest, writeQuestQst } from './data/parsing/quest'; -import { parseNj, parseXj} from './data/parsing/ninja'; -import { getAreaSections } from './data/loading/areas'; -import { getNpcGeometry, getObjectGeometry } from './data/loading/entities'; -import { createObjectMesh, createNpcMesh } from './rendering/entities'; -import { createModelMesh } from './rendering/models'; -import { VisibleQuestEntity } from './domain'; - -export function entitySelected(entity?: VisibleQuestEntity) { - applicationState.selectedEntity = entity; -} - -export function loadFile(file: File) { - const reader = new FileReader(); - - reader.addEventListener('loadend', async () => { - if (!(reader.result instanceof ArrayBuffer)) { - console.error('Couldn\'t read file.'); - return; - } - - if (file.name.endsWith('.nj')) { - // Reset application state, then set the current model. - // Might want to do this in a MobX transaction. - resetModelAndQuestState(); - applicationState.currentModel = createModelMesh(parseNj(new ArrayBufferCursor(reader.result, true))); - } else if (file.name.endsWith('.xj')) { - // Reset application state, then set the current model. - // Might want to do this in a MobX transaction. - resetModelAndQuestState(); - applicationState.currentModel = createModelMesh(parseXj(new ArrayBufferCursor(reader.result, true))); - } else { - const quest = parseQuest(new ArrayBufferCursor(reader.result, true)); - - if (quest) { - // Reset application state, then set current quest and area in the correct order. - // Might want to do this in a MobX transaction. - resetModelAndQuestState(); - applicationState.currentQuest = quest; - - if (quest.areaVariants.length) { - applicationState.currentArea = quest.areaVariants[0].area; - } - - // Load section data. - for (const variant of quest.areaVariants) { - const sections = await getAreaSections(quest.episode, variant.area.id, variant.id) - variant.sections = sections; - - // Generate object geometry. - for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) { - try { - const geometry = await getObjectGeometry(object.type); - object.object3d = createObjectMesh(object, sections, geometry); - } catch (e) { - console.error(e); - } - } - - // Generate NPC geometry. - for (const npc of quest.npcs.filter(npc => npc.areaId === variant.area.id)) { - try { - const geometry = await getNpcGeometry(npc.type); - npc.object3d = createNpcMesh(npc, sections, geometry); - } catch (e) { - console.error(e); - } - } - } - } - } - }); - - reader.readAsArrayBuffer(file); -} - -export function currentAreaIdChanged(areaId?: number) { - applicationState.selectedEntity = undefined; - - if (areaId == null) { - applicationState.currentArea = undefined; - } else if (applicationState.currentQuest) { - const areaVariant = applicationState.currentQuest.areaVariants.find( - variant => variant.area.id === areaId); - applicationState.currentArea = areaVariant && areaVariant.area; - } -} - -export function saveCurrentQuestToFile(fileName: string) { - if (applicationState.currentQuest) { - const cursor = writeQuestQst(applicationState.currentQuest, fileName); - - if (!fileName.endsWith('.qst')) { - fileName += '.qst'; - } - - const a = document.createElement('a'); - a.href = URL.createObjectURL(new Blob([cursor.buffer])); - a.download = fileName; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); - } -} - -function resetModelAndQuestState() { - applicationState.currentQuest = undefined; - applicationState.currentArea = undefined; - applicationState.selectedEntity = undefined; - applicationState.currentModel = undefined; -} diff --git a/src/actions/appState.ts b/src/actions/appState.ts new file mode 100644 index 00000000..0dda5416 --- /dev/null +++ b/src/actions/appState.ts @@ -0,0 +1,66 @@ +import { writeQuestQst } from '../data/parsing/quest'; +import { VisibleQuestEntity, Quest } from '../domain'; +import { appStateStore } from '../stores/AppStateStore'; +import { action } from 'mobx'; +import { Object3D } from 'three'; + +/** + * Reset application state, then set the current model. + */ +export const setModel = action('setModel', (model?: Object3D) => { + resetModelAndQuestState(); + appStateStore.currentModel = model; +}); + +/** + * Reset application state, then set current quest and area. + */ +export const setQuest = action('setQuest', (quest?: Quest) => { + resetModelAndQuestState(); + appStateStore.currentQuest = quest; + + if (quest && quest.areaVariants.length) { + appStateStore.currentArea = quest.areaVariants[0].area; + } +}); + +function resetModelAndQuestState() { + appStateStore.currentQuest = undefined; + appStateStore.currentArea = undefined; + appStateStore.selectedEntity = undefined; + appStateStore.currentModel = undefined; +} + +export const setSelectedEntity = action('setSelectedEntity', (entity?: VisibleQuestEntity) => { + appStateStore.selectedEntity = entity; +}); + +export const setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => { + appStateStore.selectedEntity = undefined; + + if (areaId == null) { + appStateStore.currentArea = undefined; + } else if (appStateStore.currentQuest) { + const areaVariant = appStateStore.currentQuest.areaVariants.find( + variant => variant.area.id === areaId); + appStateStore.currentArea = areaVariant && areaVariant.area; + } +}); + +export const saveCurrentQuestToFile = (fileName: string) => { + if (appStateStore.currentQuest) { + const cursor = writeQuestQst(appStateStore.currentQuest, fileName); + + if (!fileName.endsWith('.qst')) { + fileName += '.qst'; + } + + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob([cursor.buffer])); + a.download = fileName; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(a.href); + document.body.removeChild(a); + } +}; diff --git a/src/actions/loadFile.ts b/src/actions/loadFile.ts new file mode 100644 index 00000000..efe9323d --- /dev/null +++ b/src/actions/loadFile.ts @@ -0,0 +1,100 @@ +import { action } from 'mobx'; +import { Object3D } from 'three'; +import { ArrayBufferCursor } from '../data/ArrayBufferCursor'; +import { getAreaSections } from '../data/loading/areas'; +import { getNpcGeometry, getObjectGeometry } from '../data/loading/entities'; +import { parseNj, parseXj } from '../data/parsing/ninja'; +import { parseQuest } from '../data/parsing/quest'; +import { AreaVariant, Section, Vec3, VisibleQuestEntity } from '../domain'; +import { createNpcMesh, createObjectMesh } from '../rendering/entities'; +import { createModelMesh } from '../rendering/models'; +import { setModel, setQuest } from './appState'; + +export function loadFile(file: File) { + const reader = new FileReader(); + reader.addEventListener('loadend', () => { loadend(file, reader) }); + reader.readAsArrayBuffer(file); +} + +// TODO: notify user of problems. +async function loadend(file: File, reader: FileReader) { + if (!(reader.result instanceof ArrayBuffer)) { + console.error('Couldn\'t read file.'); + return; + } + + if (file.name.endsWith('.nj')) { + setModel(createModelMesh(parseNj(new ArrayBufferCursor(reader.result, true)))); + } else if (file.name.endsWith('.xj')) { + setModel(createModelMesh(parseXj(new ArrayBufferCursor(reader.result, true)))); + } else { + const quest = parseQuest(new ArrayBufferCursor(reader.result, true)); + setQuest(quest); + + if (quest) { + // Load section data. + for (const variant of quest.areaVariants) { + const sections = await getAreaSections(quest.episode, variant.area.id, variant.id); + setSectionsOnAreaVariant(variant, sections); + + // Generate object geometry. + for (const object of quest.objects.filter(o => o.areaId === variant.area.id)) { + try { + const geometry = await getObjectGeometry(object.type); + setSectionOnVisibleQuestEntity(object, sections); + setObject3dOnVisibleQuestEntity(object, createObjectMesh(object, geometry)); + } catch (e) { + console.error(e); + } + } + + // Generate NPC geometry. + for (const npc of quest.npcs.filter(npc => npc.areaId === variant.area.id)) { + try { + const geometry = await getNpcGeometry(npc.type); + setSectionOnVisibleQuestEntity(npc, sections); + setObject3dOnVisibleQuestEntity(npc, createNpcMesh(npc, geometry)); + } catch (e) { + console.error(e); + } + } + } + } else { + console.error('Couldn\'t parse quest file.'); + } + } +} + +const setSectionsOnAreaVariant = action('setSectionsOnAreaVariant', + (variant: AreaVariant, sections: Section[]) => { + variant.sections = sections; + } +); + +const setSectionOnVisibleQuestEntity = action('setSectionOnVisibleQuestEntity', + (entity: VisibleQuestEntity, sections: Section[]) => { + let { x, y, z } = entity.position; + + const section = sections.find(s => s.id === entity.sectionId); + entity.section = section; + + if (section) { + const { x: secX, y: secY, z: secZ } = section.position; + const rotX = section.cosYAxisRotation * x + section.sinYAxisRotation * z; + const rotZ = -section.sinYAxisRotation * x + section.cosYAxisRotation * z; + x = rotX + secX; + y += secY; + z = rotZ + secZ; + } else { + console.warn(`Section ${entity.sectionId} not found.`); + } + + entity.position = new Vec3(x, y, z); + } +); + +const setObject3dOnVisibleQuestEntity = action('setObject3dOnVisibleQuestEntity', + (entity: VisibleQuestEntity, object3d: Object3D) => { + entity.object3d = object3d; + } +); diff --git a/src/actions/visibleQuestEntities.ts b/src/actions/visibleQuestEntities.ts new file mode 100644 index 00000000..b89720e2 --- /dev/null +++ b/src/actions/visibleQuestEntities.ts @@ -0,0 +1,12 @@ +import { action } from "mobx"; +import { VisibleQuestEntity, Vec3, Section } from "../domain"; + +export const setPositionOnVisibleQuestEntity = action('setPositionOnVisibleQuestEntity', + (entity: VisibleQuestEntity, position: Vec3, section?: Section) => { + entity.position = position; + + if (section) { + entity.section = section; + } + } +); \ No newline at end of file diff --git a/src/data/parsing/qst.ts b/src/data/parsing/qst.ts index 58ffd885..119d279d 100644 --- a/src/data/parsing/qst.ts +++ b/src/data/parsing/qst.ts @@ -18,7 +18,7 @@ interface ParseQstResult { * Low level parsing function for .qst files. * Can only read the Blue Burst format. */ -export function parseQst(cursor: ArrayBufferCursor): ParseQstResult | null { +export function parseQst(cursor: ArrayBufferCursor): ParseQstResult | undefined { // A .qst file contains two 88-byte headers that describe the embedded .dat and .bin files. let version = 'PC'; @@ -60,7 +60,7 @@ export function parseQst(cursor: ArrayBufferCursor): ParseQstResult | null { files }; } else { - return null; + console.error(`Can't parse ${version} QST files.`); } } @@ -206,6 +206,10 @@ function parseFiles(cursor: ArrayBufferCursor, expectedSizes: Map 16) { + throw Error(`File ${file.name} has a name longer than 16 characters.`); + } + cursor.writeU16(88); // Header size. cursor.writeU16(0x44); // Magic number. cursor.writeU16(file.questNo || 0); @@ -229,6 +233,10 @@ function writeFileHeaders(cursor: ArrayBufferCursor, files: SimpleQstContainedFi fileName2 = file.name2; } + if (fileName2.length > 24) { + throw Error(`File ${file.name} has a fileName2 length (${fileName2}) longer than 24 characters.`); + } + cursor.writeStringAscii(fileName2, 24); } } diff --git a/src/data/parsing/quest.ts b/src/data/parsing/quest.ts index 18afb63f..8f9d277d 100644 --- a/src/data/parsing/quest.ts +++ b/src/data/parsing/quest.ts @@ -12,18 +12,18 @@ import { ObjectType, NpcType } from '../../domain'; -import { areaStore } from '../../store'; +import { areaStore } from '../../stores/AreaStore'; /** * High level parsing function that delegates to lower level parsing functions. * * Always delegates to parseQst at the moment. */ -export function parseQuest(cursor: ArrayBufferCursor): Quest | null { +export function parseQuest(cursor: ArrayBufferCursor): Quest | undefined { const qst = parseQst(cursor); if (!qst) { - return null; + return; } let datFile = null; @@ -39,8 +39,14 @@ export function parseQuest(cursor: ArrayBufferCursor): Quest | null { // TODO: deal with missing/multiple DAT or BIN file. - if (!datFile || !binFile) { - return null; + if (!datFile) { + console.error('File contains no DAT file.'); + return; + } + + if (!binFile) { + console.error('File contains no BIN file.'); + return; } const dat = parseDat(prs.decompress(datFile.data)); diff --git a/src/index.tsx b/src/index.tsx index d65b3206..e3f55910 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,11 @@ import './index.css'; import "normalize.css"; import "@blueprintjs/core/lib/css/blueprint.css"; import "@blueprintjs/icons/lib/css/blueprint-icons.css"; +import { configure } from 'mobx'; + +configure({ + enforceActions: 'observed' +}); ReactDOM.render( , diff --git a/src/rendering/Renderer.ts b/src/rendering/Renderer.ts index 17b0bdaf..1a33ef10 100644 --- a/src/rendering/Renderer.ts +++ b/src/rendering/Renderer.ts @@ -26,11 +26,11 @@ import { NPC_HOVER_COLOR, NPC_SELECTED_COLOR } from './entities'; +import { setSelectedEntity } from '../actions/appState'; +import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/visibleQuestEntities'; const OrbitControls = OrbitControlsCreator(THREE); -type OnSelectCallback = (visibleQuestEntity: VisibleQuestEntity | undefined) => void; - interface PickEntityResult { object: Mesh; entity: VisibleQuestEntity; @@ -58,14 +58,11 @@ export class Renderer { private renderGeometry = new Object3D(); private objGeometry = new Object3D(); private npcGeometry = new Object3D(); - private onSelect?: OnSelectCallback; private hoveredData?: PickEntityResult; private selectedData?: PickEntityResult; private model?: Object3D; - constructor({ onSelect }: { onSelect: OnSelectCallback }) { - this.onSelect = onSelect; - + constructor() { this.renderer.domElement.addEventListener( 'mousedown', this.onMouseDown); this.renderer.domElement.addEventListener( @@ -266,8 +263,8 @@ export class Renderer { ? oldSelectedData.object !== data.object : oldSelectedData !== data; - if (selectionChanged && this.onSelect) { - this.onSelect(data && data.entity); + if (selectionChanged) { + setSelectedEntity(data && data.entity); } } @@ -282,7 +279,7 @@ export class Renderer { const pointerPos = this.pointerPosToDeviceCoords(e); if (this.selectedData && this.selectedData.manipulating) { - if (e.button === 0) { + if (e.buttons === 1) { // User is dragging a selected entity. const data = this.selectedData; @@ -302,27 +299,23 @@ export class Renderer { const yDelta = y - data.entity.position.y; data.dragY += yDelta; data.dragAdjust.y -= yDelta; - data.entity.position = new Vec3( + setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( data.entity.position.x, y, data.entity.position.z - ); + )); } } else { // Horizontal movement accross terrain. // Cast ray adjusted for dragging entities. - const { intersection: terrain, section } = this.pickTerrain(pointerPos, data); + const { intersection, section } = this.pickTerrain(pointerPos, data); - if (terrain) { - data.entity.position = new Vec3( - terrain.point.x, - terrain.point.y + data.dragY, - terrain.point.z - ); - - if (section) { - data.entity.section = section; - } + if (intersection) { + setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( + intersection.point.x, + intersection.point.y + data.dragY, + intersection.point.z + ), section); } else { // If the cursor is not over any terrain, we translate the entity accross the horizontal plane in which the entity's origin lies. this.raycaster.setFromCamera(pointerPos, this.camera); @@ -334,11 +327,11 @@ export class Renderer { const intersectionPoint = new Vector3(); if (ray.intersectPlane(plane, intersectionPoint)) { - data.entity.position = new Vec3( + setPositionAndSectionOnVisibleQuestEntity(data.entity, new Vec3( intersectionPoint.x + data.grabOffset.x, data.entity.position.y, intersectionPoint.z + data.grabOffset.z - ); + )); } } } @@ -423,9 +416,9 @@ export class Renderer { return { object: intersection.object as Mesh, entity, - grabOffset: grabOffset, - dragAdjust: dragAdjust, - dragY: dragY, + grabOffset, + dragAdjust, + dragY, manipulating: false }; } diff --git a/src/rendering/entities.test.ts b/src/rendering/entities.test.ts index 6d4b98da..2cecd03b 100644 --- a/src/rendering/entities.test.ts +++ b/src/rendering/entities.test.ts @@ -1,53 +1,39 @@ -import { - createObjectMesh, - createNpcMesh, - OBJECT_COLOR, - NPC_COLOR -} from './entities'; -import { Object3D, Vector3, MeshLambertMaterial, CylinderBufferGeometry } from 'three'; -import { Vec3, QuestNpc, QuestObject, Section, NpcType, ObjectType } from '../domain'; -import { DatObject, DatNpc } from '../data/parsing/dat'; +import { CylinderBufferGeometry, MeshLambertMaterial, Object3D, Vector3 } from 'three'; +import { DatNpc, DatObject } from '../data/parsing/dat'; +import { NpcType, ObjectType, QuestNpc, QuestObject, Vec3 } from '../domain'; +import { createNpcMesh, createObjectMesh, NPC_COLOR, OBJECT_COLOR } from './entities'; const cylinder = new CylinderBufferGeometry(3, 3, 20).translate(0, 10, 0); test('create geometry for quest objects', () => { const object = new QuestObject(7, 13, new Vec3(17, 19, 23), new Vec3(), ObjectType.PrincipalWarp, {} as DatObject); - const sectRot = 0.6; - const sectRotSin = Math.sin(sectRot); - const sectRotCos = Math.cos(sectRot); - const geometry = createObjectMesh( - object, [new Section(13, new Vec3(29, 31, 37), sectRot)], cylinder); + const geometry = createObjectMesh(object, cylinder); expect(geometry).toBeInstanceOf(Object3D); expect(geometry.name).toBe('Object'); expect(geometry.userData.entity).toBe(object); - expect(geometry.position.x).toBe(sectRotCos * 17 + sectRotSin * 23 + 29); - expect(geometry.position.y).toBe(19 + 31); - expect(geometry.position.z).toBe(-sectRotSin * 17 + sectRotCos * 23 + 37); + expect(geometry.position.x).toBe(17); + expect(geometry.position.y).toBe(19); + expect(geometry.position.z).toBe(23); expect((geometry.material as MeshLambertMaterial).color.getHex()).toBe(OBJECT_COLOR); }); test('create geometry for quest NPCs', () => { const npc = new QuestNpc(7, 13, new Vec3(17, 19, 23), new Vec3(), NpcType.Booma, {} as DatNpc); - const sectRot = 0.6; - const sectRotSin = Math.sin(sectRot); - const sectRotCos = Math.cos(sectRot); - const geometry = createNpcMesh( - npc, [new Section(13, new Vec3(29, 31, 37), sectRot)], cylinder); + const geometry = createNpcMesh(npc, cylinder); expect(geometry).toBeInstanceOf(Object3D); expect(geometry.name).toBe('NPC'); expect(geometry.userData.entity).toBe(npc); - expect(geometry.position.x).toBe(sectRotCos * 17 + sectRotSin * 23 + 29); - expect(geometry.position.y).toBe(19 + 31); - expect(geometry.position.z).toBe(-sectRotSin * 17 + sectRotCos * 23 + 37); + expect(geometry.position.x).toBe(17); + expect(geometry.position.y).toBe(19); + expect(geometry.position.z).toBe(23); expect((geometry.material as MeshLambertMaterial).color.getHex()).toBe(NPC_COLOR); }); test('geometry position changes when entity position changes element-wise', () => { const npc = new QuestNpc(7, 13, new Vec3(17, 19, 23), new Vec3(), NpcType.Booma, {} as DatNpc); - const geometry = createNpcMesh( - npc, [new Section(13, new Vec3(0, 0, 0), 0)], cylinder); + const geometry = createNpcMesh(npc, cylinder); npc.position = new Vec3(2, 3, 5).add(npc.position); expect(geometry.position).toEqual(new Vector3(19, 22, 28)); @@ -55,8 +41,7 @@ test('geometry position changes when entity position changes element-wise', () = test('geometry position changes when entire entity position changes', () => { const npc = new QuestNpc(7, 13, new Vec3(17, 19, 23), new Vec3(), NpcType.Booma, {} as DatNpc); - const geometry = createNpcMesh( - npc, [new Section(13, new Vec3(0, 0, 0), 0)], cylinder); + const geometry = createNpcMesh(npc, cylinder); npc.position = new Vec3(2, 3, 5); expect(geometry.position).toEqual(new Vector3(2, 3, 5)); diff --git a/src/rendering/entities.ts b/src/rendering/entities.ts index 7de5ddc4..893649e7 100644 --- a/src/rendering/entities.ts +++ b/src/rendering/entities.ts @@ -1,6 +1,6 @@ -import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from 'three'; import { autorun } from 'mobx'; -import { Vec3, VisibleQuestEntity, QuestNpc, QuestObject, Section } from '../domain'; +import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from 'three'; +import { QuestNpc, QuestObject, VisibleQuestEntity } from '../domain'; export const OBJECT_COLOR = 0xFFFF00; export const OBJECT_HOVER_COLOR = 0xFFDF3F; @@ -9,37 +9,20 @@ export const NPC_COLOR = 0xFF0000; export const NPC_HOVER_COLOR = 0xFF3F5F; export const NPC_SELECTED_COLOR = 0xFF0054; -export function createObjectMesh(object: QuestObject, sections: Section[], geometry: BufferGeometry): Mesh { - return createMesh(object, sections, geometry, OBJECT_COLOR, 'Object'); +export function createObjectMesh(object: QuestObject, geometry: BufferGeometry): Mesh { + return createMesh(object, geometry, OBJECT_COLOR, 'Object'); } -export function createNpcMesh(npc: QuestNpc, sections: Section[], geometry: BufferGeometry): Mesh { - return createMesh(npc, sections, geometry, NPC_COLOR, 'NPC'); +export function createNpcMesh(npc: QuestNpc, geometry: BufferGeometry): Mesh { + return createMesh(npc, geometry, NPC_COLOR, 'NPC'); } function createMesh( entity: VisibleQuestEntity, - sections: Section[], geometry: BufferGeometry, color: number, type: string ): Mesh { - let {x, y, z} = entity.position; - - const section = sections.find(s => s.id === entity.sectionId); - entity.section = section; - - if (section) { - const {x: secX, y: secY, z: secZ} = section.position; - const rotX = section.cosYAxisRotation * x + section.sinYAxisRotation * z; - const rotZ = -section.sinYAxisRotation * x + section.cosYAxisRotation * z; - x = rotX + secX; - y += secY; - z = rotZ + secZ; - } else { - console.warn(`Section ${entity.sectionId} not found.`); - } - const object3d = new Mesh( geometry, new MeshLambertMaterial({ @@ -52,13 +35,11 @@ function createMesh( // TODO: dispose autorun? autorun(() => { - const {x, y, z} = entity.position; + const { x, y, z } = entity.position; object3d.position.set(x, y, z); const rot = entity.rotation; object3d.rotation.set(rot.x, rot.y, rot.z); }); - entity.position = new Vec3(x, y, z); - return object3d; } diff --git a/src/stores/AppStateStore.ts b/src/stores/AppStateStore.ts new file mode 100644 index 00000000..63c1d3bb --- /dev/null +++ b/src/stores/AppStateStore.ts @@ -0,0 +1,12 @@ +import { observable } from 'mobx'; +import { Object3D } from 'three'; +import { Area, Quest, VisibleQuestEntity } from '../domain'; + +class AppStateStore { + @observable currentModel?: Object3D; + @observable currentQuest?: Quest; + @observable currentArea?: Area; + @observable selectedEntity?: VisibleQuestEntity; +} + +export const appStateStore = new AppStateStore(); diff --git a/src/store.ts b/src/stores/AreaStore.ts similarity index 89% rename from src/store.ts rename to src/stores/AreaStore.ts index 48e0d405..66974fbf 100644 --- a/src/store.ts +++ b/src/stores/AreaStore.ts @@ -1,6 +1,4 @@ -import { observable } from 'mobx'; -import { Object3D } from 'three'; -import { Area, AreaVariant, Quest, VisibleQuestEntity } from './domain'; +import { Area, AreaVariant } from '../domain'; function area(id: number, name: string, order: number, variants: number) { const area = new Area(id, name, order, []); @@ -86,12 +84,3 @@ class AreaStore { } export const areaStore = new AreaStore(); - -class ApplicationState { - @observable currentModel?: Object3D; - @observable currentQuest?: Quest; - @observable currentArea?: Area; - @observable selectedEntity?: VisibleQuestEntity; -} - -export const applicationState = new ApplicationState(); diff --git a/src/ui/ApplicationComponent.tsx b/src/ui/ApplicationComponent.tsx index 11acf8f8..a52e4e01 100644 --- a/src/ui/ApplicationComponent.tsx +++ b/src/ui/ApplicationComponent.tsx @@ -1,31 +1,32 @@ -import React, { ChangeEvent, KeyboardEvent } from 'react'; -import { observer } from 'mobx-react'; import { Button, Dialog, Intent } from '@blueprintjs/core'; -import { applicationState } from '../store'; -import { currentAreaIdChanged, loadFile, saveCurrentQuestToFile } from '../actions'; -import { Area3DComponent } from './Area3DComponent'; +import { observer } from 'mobx-react'; +import React, { ChangeEvent, KeyboardEvent } from 'react'; +import { saveCurrentQuestToFile, setCurrentAreaId } from '../actions/appState'; +import { loadFile } from '../actions/loadFile'; +import { appStateStore } from '../stores/AppStateStore'; +import './ApplicationComponent.css'; +import { RendererComponent } from './RendererComponent'; import { EntityInfoComponent } from './EntityInfoComponent'; import { QuestInfoComponent } from './QuestInfoComponent'; -import './ApplicationComponent.css'; @observer export class ApplicationComponent extends React.Component<{}, { filename?: string, - save_dialog_open: boolean, - save_dialog_filename: string + saveDialogOpen: boolean, + saveDialogFilename: string }> { state = { filename: undefined, - save_dialog_open: false, - save_dialog_filename: 'Untitled' + saveDialogOpen: false, + saveDialogFilename: 'Untitled', }; render() { - const quest = applicationState.currentQuest; - const model = applicationState.currentModel; + const quest = appStateStore.currentQuest; + const model = appStateStore.currentModel; const areas = quest ? Array.from(quest.areaVariants).map(a => a.area) : undefined; - const area = applicationState.currentArea; - const area_id = area ? String(area.id) : undefined; + const area = appStateStore.currentArea; + const areaId = area ? String(area.id) : undefined; return (
@@ -39,7 +40,7 @@ export class ApplicationComponent extends React.Component<{}, { + onChange={this.onFileChange} /> {this.state.filename || 'Choose file...'} @@ -50,8 +51,8 @@ export class ApplicationComponent extends React.Component<{}, { ? (
@@ -62,26 +63,26 @@ export class ApplicationComponent extends React.Component<{}, { text="Save as..." icon="floppy-disk" style={{ marginLeft: 10 }} - onClick={this._on_save_as_click} /> + onClick={this.onSaveAsClick} /> : null}
- - +
+ isOpen={this.state.saveDialogOpen} + onClose={this.onSaveDialogClose}>
@@ -100,7 +103,7 @@ export class ApplicationComponent extends React.Component<{}, {
@@ -109,7 +112,7 @@ export class ApplicationComponent extends React.Component<{}, { ); } - private _on_file_change = (e: ChangeEvent) => { + private onFileChange = (e: ChangeEvent) => { if (e.currentTarget.files) { const file = e.currentTarget.files[0]; @@ -122,37 +125,37 @@ export class ApplicationComponent extends React.Component<{}, { } } - private _on_area_select_change = (e: ChangeEvent) => { - const area_id = parseInt(e.currentTarget.value, 10); - currentAreaIdChanged(area_id); + private onAreaSelectChange = (e: ChangeEvent) => { + const areaId = parseInt(e.currentTarget.value, 10); + setCurrentAreaId(areaId); } - private _on_save_as_click = () => { + private onSaveAsClick = () => { let name = this.state.filename || 'Untitled'; name = name.endsWith('.qst') ? name.slice(0, -4) : name; this.setState({ - save_dialog_open: true, - save_dialog_filename: name + saveDialogOpen: true, + saveDialogFilename: name }); } - private _on_save_dialog_name_change = (e: ChangeEvent) => { - this.setState({ save_dialog_filename: e.currentTarget.value }); + private onSaveDialogNameChange = (e: ChangeEvent) => { + this.setState({ saveDialogFilename: e.currentTarget.value }); } - private _on_save_dialog_name_key_up = (e: KeyboardEvent) => { + private onSaveDialogNameKeyUp = (e: KeyboardEvent) => { if (e.key === 'Enter') { - this._on_save_dialog_save_click(); + this.onSaveDialogSaveClick(); } } - private _on_save_dialog_save_click = () => { - saveCurrentQuestToFile(this.state.save_dialog_filename); - this.setState({ save_dialog_open: false }); + private onSaveDialogSaveClick = () => { + saveCurrentQuestToFile(this.state.saveDialogFilename); + this.setState({ saveDialogOpen: false }); } - private _on_save_dialog_close = () => { - this.setState({ save_dialog_open: false }); + private onSaveDialogClose = () => { + this.setState({ saveDialogOpen: false }); } } diff --git a/src/ui/Area3DComponent.tsx b/src/ui/RendererComponent.tsx similarity index 67% rename from src/ui/Area3DComponent.tsx rename to src/ui/RendererComponent.tsx index 511a1978..938aa7f6 100644 --- a/src/ui/Area3DComponent.tsx +++ b/src/ui/RendererComponent.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Object3D } from 'three'; -import { entitySelected } from '../actions'; +import { Area, Quest } from '../domain'; import { Renderer } from '../rendering/Renderer'; -import { Area, Quest, VisibleQuestEntity } from '../domain'; interface Props { quest?: Quest; @@ -10,17 +9,8 @@ interface Props { model?: Object3D; } -export class Area3DComponent extends React.Component { - private renderer: Renderer; - - constructor(props: Props) { - super(props); - - // renderer has to be assigned here so that it happens after onSelect is assigned. - this.renderer = new Renderer({ - onSelect: this.onSelect - }); - } +export class RendererComponent extends React.Component { + private renderer = new Renderer(); render() { return
; @@ -51,10 +41,6 @@ export class Area3DComponent extends React.Component { div.appendChild(this.renderer.domElement); } - private onSelect = (entity?: VisibleQuestEntity) => { - entitySelected(entity); - } - private onResize = () => { const wrapperDiv = this.renderer.domElement.parentNode as HTMLDivElement; this.renderer.setSize(wrapperDiv.clientWidth, wrapperDiv.clientHeight);