Refactored code so that new tools can now be added easily.

This commit is contained in:
Daan Vanden Bosch 2019-05-29 21:43:06 +02:00
parent eacf826fc8
commit f31570d5f5
14 changed files with 229 additions and 174 deletions

View File

@ -1,14 +1,14 @@
import { action } from 'mobx'; import { action } from 'mobx';
import { Object3D } from 'three'; import { Object3D } from 'three';
import { ArrayBufferCursor } from '../data/ArrayBufferCursor'; import { ArrayBufferCursor } from '../../data/ArrayBufferCursor';
import { getAreaSections } from '../data/loading/areas'; import { getAreaSections } from '../../data/loading/areas';
import { getNpcGeometry, getObjectGeometry } from '../data/loading/entities'; import { getNpcGeometry, getObjectGeometry } from '../../data/loading/entities';
import { parseNj, parseXj } from '../data/parsing/ninja'; import { parseNj, parseXj } from '../../data/parsing/ninja';
import { parseQuest } from '../data/parsing/quest'; import { parseQuest } from '../../data/parsing/quest';
import { AreaVariant, Section, Vec3, VisibleQuestEntity } from '../domain'; import { AreaVariant, Section, Vec3, VisibleQuestEntity } from '../../domain';
import { createNpcMesh, createObjectMesh } from '../rendering/entities'; import { createNpcMesh, createObjectMesh } from '../../rendering/entities';
import { createModelMesh } from '../rendering/models'; import { createModelMesh } from '../../rendering/models';
import { setModel, setQuest } from './appState'; import { setModel, setQuest } from './questEditor';
export function loadFile(file: File) { export function loadFile(file: File) {
const reader = new FileReader(); const reader = new FileReader();

View File

@ -1,6 +1,6 @@
import { writeQuestQst } from '../data/parsing/quest'; import { writeQuestQst } from '../../data/parsing/quest';
import { VisibleQuestEntity, Quest } from '../domain'; import { VisibleQuestEntity, Quest } from '../../domain';
import { appStateStore } from '../stores/AppStateStore'; import { questEditorStore } from '../../stores/QuestEditorStore';
import { action } from 'mobx'; import { action } from 'mobx';
import { Object3D } from 'three'; import { Object3D } from 'three';
@ -9,7 +9,7 @@ import { Object3D } from 'three';
*/ */
export const setModel = action('setModel', (model?: Object3D) => { export const setModel = action('setModel', (model?: Object3D) => {
resetModelAndQuestState(); resetModelAndQuestState();
appStateStore.currentModel = model; questEditorStore.currentModel = model;
}); });
/** /**
@ -17,39 +17,39 @@ export const setModel = action('setModel', (model?: Object3D) => {
*/ */
export const setQuest = action('setQuest', (quest?: Quest) => { export const setQuest = action('setQuest', (quest?: Quest) => {
resetModelAndQuestState(); resetModelAndQuestState();
appStateStore.currentQuest = quest; questEditorStore.currentQuest = quest;
if (quest && quest.areaVariants.length) { if (quest && quest.areaVariants.length) {
appStateStore.currentArea = quest.areaVariants[0].area; questEditorStore.currentArea = quest.areaVariants[0].area;
} }
}); });
function resetModelAndQuestState() { function resetModelAndQuestState() {
appStateStore.currentQuest = undefined; questEditorStore.currentQuest = undefined;
appStateStore.currentArea = undefined; questEditorStore.currentArea = undefined;
appStateStore.selectedEntity = undefined; questEditorStore.selectedEntity = undefined;
appStateStore.currentModel = undefined; questEditorStore.currentModel = undefined;
} }
export const setSelectedEntity = action('setSelectedEntity', (entity?: VisibleQuestEntity) => { export const setSelectedEntity = action('setSelectedEntity', (entity?: VisibleQuestEntity) => {
appStateStore.selectedEntity = entity; questEditorStore.selectedEntity = entity;
}); });
export const setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => { export const setCurrentAreaId = action('setCurrentAreaId', (areaId?: number) => {
appStateStore.selectedEntity = undefined; questEditorStore.selectedEntity = undefined;
if (areaId == null) { if (areaId == null) {
appStateStore.currentArea = undefined; questEditorStore.currentArea = undefined;
} else if (appStateStore.currentQuest) { } else if (questEditorStore.currentQuest) {
const areaVariant = appStateStore.currentQuest.areaVariants.find( const areaVariant = questEditorStore.currentQuest.areaVariants.find(
variant => variant.area.id === areaId); variant => variant.area.id === areaId);
appStateStore.currentArea = areaVariant && areaVariant.area; questEditorStore.currentArea = areaVariant && areaVariant.area;
} }
}); });
export const saveCurrentQuestToFile = (fileName: string) => { export const saveCurrentQuestToFile = (fileName: string) => {
if (appStateStore.currentQuest) { if (questEditorStore.currentQuest) {
const cursor = writeQuestQst(appStateStore.currentQuest, fileName); const cursor = writeQuestQst(questEditorStore.currentQuest, fileName);
if (!fileName.endsWith('.qst')) { if (!fileName.endsWith('.qst')) {
fileName += '.qst'; fileName += '.qst';

View File

@ -1,5 +1,5 @@
import { action } from "mobx"; import { action } from "mobx";
import { VisibleQuestEntity, Vec3, Section } from "../domain"; import { VisibleQuestEntity, Vec3, Section } from "../../domain";
export const setPositionOnVisibleQuestEntity = action('setPositionOnVisibleQuestEntity', export const setPositionOnVisibleQuestEntity = action('setPositionOnVisibleQuestEntity',
(entity: VisibleQuestEntity, position: Vec3, section?: Section) => { (entity: VisibleQuestEntity, position: Vec3, section?: Section) => {

View File

@ -26,8 +26,8 @@ import {
NPC_HOVER_COLOR, NPC_HOVER_COLOR,
NPC_SELECTED_COLOR NPC_SELECTED_COLOR
} from './entities'; } from './entities';
import { setSelectedEntity } from '../actions/appState'; import { setSelectedEntity } from '../actions/quest-editor/questEditor';
import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/visibleQuestEntities'; import { setPositionOnVisibleQuestEntity as setPositionAndSectionOnVisibleQuestEntity } from '../actions/quest-editor/visibleQuestEntities';
const OrbitControls = OrbitControlsCreator(THREE); const OrbitControls = OrbitControlsCreator(THREE);
@ -41,7 +41,7 @@ interface PickEntityResult {
} }
/** /**
* Renders one quest area at a time. * Renders a quest area or an NJ/XJ model.
*/ */
export class Renderer { export class Renderer {
private renderer = new WebGLRenderer({ antialias: true }); private renderer = new WebGLRenderer({ antialias: true });

View File

@ -2,11 +2,11 @@ import { observable } from 'mobx';
import { Object3D } from 'three'; import { Object3D } from 'three';
import { Area, Quest, VisibleQuestEntity } from '../domain'; import { Area, Quest, VisibleQuestEntity } from '../domain';
class AppStateStore { class QuestEditorStore {
@observable currentModel?: Object3D; @observable currentModel?: Object3D;
@observable currentQuest?: Quest; @observable currentQuest?: Quest;
@observable currentArea?: Area; @observable currentArea?: Area;
@observable selectedEntity?: VisibleQuestEntity; @observable selectedEntity?: VisibleQuestEntity;
} }
export const appStateStore = new AppStateStore(); export const questEditorStore = new QuestEditorStore();

View File

@ -8,8 +8,8 @@
right: 0; right: 0;
} }
.ApplicationComponent-heading { div.ApplicationComponent .ApplicationComponent-heading {
font-size: 22px !important; font-size: 22px;
} }
.ApplicationComponent-beta { .ApplicationComponent-beta {
@ -18,16 +18,13 @@
margin-left: 2; margin-left: 2;
} }
.ApplicationComponent-button-bar > * {
margin-right: 10px;
}
.ApplicationComponent-main { .ApplicationComponent-main {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: stretch;
overflow: hidden; overflow: hidden;
} }
.ApplicationComponent-main div:nth-child(2) { .ApplicationComponent-main>* {
flex: 1; flex: 1;
} }

View File

@ -1,32 +1,22 @@
import { Button, Dialog, Intent, Classes, Navbar, NavbarGroup, NavbarHeading, FileInput, HTMLSelect, FormGroup, InputGroup } from '@blueprintjs/core'; import { Classes, Navbar, NavbarGroup, NavbarHeading, Button } from '@blueprintjs/core';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { ChangeEvent, KeyboardEvent } from 'react'; import React from 'react';
import { saveCurrentQuestToFile, setCurrentAreaId } from '../actions/appState';
import { loadFile } from '../actions/loadFile';
import { appStateStore } from '../stores/AppStateStore';
import './ApplicationComponent.css'; import './ApplicationComponent.css';
import { RendererComponent } from './RendererComponent'; import { QuestEditorComponent } from './quest-editor/QuestEditorComponent';
import { EntityInfoComponent } from './EntityInfoComponent'; import { observable, action } from 'mobx';
import { QuestInfoComponent } from './QuestInfoComponent';
@observer @observer
export class ApplicationComponent extends React.Component<{}, { export class ApplicationComponent extends React.Component {
filename?: string, @observable private tool = 'quest-editor';
saveDialogOpen: boolean,
saveDialogFilename: string
}> {
state = {
filename: undefined,
saveDialogOpen: false,
saveDialogFilename: 'Untitled',
};
render() { render() {
const quest = appStateStore.currentQuest; let toolComponent;
const model = appStateStore.currentModel;
const areas = quest && Array.from(quest.areaVariants).map(a => a.area); switch (this.tool) {
const area = appStateStore.currentArea; case 'quest-editor':
const areaId = area && String(area.id); toolComponent = <QuestEditorComponent />;
break;
}
return ( return (
<div className={`ApplicationComponent ${Classes.DARK}`}> <div className={`ApplicationComponent ${Classes.DARK}`}>
@ -34,121 +24,22 @@ export class ApplicationComponent extends React.Component<{}, {
<NavbarGroup className="ApplicationComponent-button-bar"> <NavbarGroup className="ApplicationComponent-button-bar">
<NavbarHeading className="ApplicationComponent-heading"> <NavbarHeading className="ApplicationComponent-heading">
Phantasmal World Phantasmal World
<sup className="ApplicationComponent-beta">BETA</sup>
</NavbarHeading> </NavbarHeading>
<FileInput
text={this.state.filename || 'Choose file...'}
inputProps={{
type: 'file',
accept: '.nj, .qst, .xj',
onChange: this.onFileChange
}}
/>
{areas ? (
<HTMLSelect
onChange={this.onAreaSelectChange}
defaultValue={areaId}
>
{areas.map(area =>
<option key={area.id} value={area.id}>{area.name}</option>
)}
</HTMLSelect>
) : null}
{quest ? (
<Button <Button
text="Save as..." text="Quest Editor (Beta)"
icon="floppy-disk" minimal={true}
onClick={this.onSaveAsClick} onClick={() => this.setTool('quest-editor')}
/> />
) : null}
</NavbarGroup> </NavbarGroup>
</Navbar> </Navbar>
<div className="ApplicationComponent-main"> <div className="ApplicationComponent-main">
<QuestInfoComponent {toolComponent}
quest={quest} />
<RendererComponent
quest={quest}
area={area}
model={model} />
<EntityInfoComponent entity={appStateStore.selectedEntity} />
</div> </div>
<Dialog
title="Save as..."
icon="floppy-disk"
className={Classes.DARK}
style={{ width: 360 }}
isOpen={this.state.saveDialogOpen}
onClose={this.onSaveDialogClose}>
<div className={Classes.DIALOG_BODY}>
<FormGroup label="Name:" labelFor="file-name-input">
<InputGroup
id="file-name-input"
autoFocus={true}
value={this.state.saveDialogFilename}
maxLength={12}
onChange={this.onSaveDialogNameChange}
onKeyUp={this.onSaveDialogNameKeyUp}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text="Save"
style={{ marginLeft: 10 }}
onClick={this.onSaveDialogSaveClick}
intent={Intent.PRIMARY} />
</div>
</div>
</Dialog>
</div> </div>
); );
} }
private onFileChange = (e: ChangeEvent<HTMLInputElement>) => { private setTool = action('setTool', (tool: string) => {
if (e.currentTarget.files) { this.tool = tool;
const file = e.currentTarget.files[0];
if (file) {
this.setState({
filename: file.name
}); });
loadFile(file);
}
}
}
private onAreaSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
const areaId = parseInt(e.currentTarget.value, 10);
setCurrentAreaId(areaId);
}
private onSaveAsClick = () => {
let name = this.state.filename || 'Untitled';
name = name.endsWith('.qst') ? name.slice(0, -4) : name;
this.setState({
saveDialogOpen: true,
saveDialogFilename: name
});
}
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ saveDialogFilename: e.currentTarget.value });
}
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.onSaveDialogSaveClick();
}
}
private onSaveDialogSaveClick = () => {
saveCurrentQuestToFile(this.state.saveDialogFilename);
this.setState({ saveDialogOpen: false });
}
private onSaveDialogClose = () => {
this.setState({ saveDialogOpen: false });
}
} }

View File

@ -1,7 +1,7 @@
import { NumericInput } from '@blueprintjs/core'; import { NumericInput } from '@blueprintjs/core';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React from 'react'; import React from 'react';
import { QuestNpc, QuestObject, VisibleQuestEntity } from '../domain'; import { QuestNpc, QuestObject, VisibleQuestEntity } from '../../domain';
import './EntityInfoComponent.css'; import './EntityInfoComponent.css';
interface Props { interface Props {

View File

@ -0,0 +1,18 @@
.QuestEditorComponent {
display: flex;
flex-direction: column;
}
.QuestEditorComponent-main {
flex: 1;
display: flex;
overflow: hidden;
}
.QuestEditorComponent-button-bar>* {
margin-right: 10px;
}
.QuestEditorComponent-main div:nth-child(2) {
flex: 1;
}

View File

@ -0,0 +1,149 @@
import { Button, Classes, Dialog, FileInput, FormGroup, HTMLSelect, InputGroup, Intent, Navbar, NavbarGroup } from "@blueprintjs/core";
import { observer } from "mobx-react";
import React, { ChangeEvent, KeyboardEvent } from "react";
import { saveCurrentQuestToFile, setCurrentAreaId } from "../../actions/quest-editor/questEditor";
import { loadFile } from "../../actions/quest-editor/loadFile";
import { questEditorStore } from "../../stores/QuestEditorStore";
import { EntityInfoComponent } from "./EntityInfoComponent";
import './QuestEditorComponent.css';
import { QuestInfoComponent } from "./QuestInfoComponent";
import { RendererComponent } from "./RendererComponent";
@observer
export class QuestEditorComponent extends React.Component<{}, {
filename?: string,
saveDialogOpen: boolean,
saveDialogFilename: string
}> {
state = {
filename: undefined,
saveDialogOpen: false,
saveDialogFilename: 'Untitled',
};
render() {
const quest = questEditorStore.currentQuest;
const model = questEditorStore.currentModel;
const areas = quest && Array.from(quest.areaVariants).map(a => a.area);
const area = questEditorStore.currentArea;
const areaId = area && String(area.id);
return (
<div className="QuestEditorComponent">
<Navbar>
<NavbarGroup className="QuestEditorComponent-button-bar">
<FileInput
text={this.state.filename || 'Choose file...'}
inputProps={{
type: 'file',
accept: '.nj, .qst, .xj',
onChange: this.onFileChange
}}
/>
{areas ? (
<HTMLSelect
onChange={this.onAreaSelectChange}
defaultValue={areaId}
>
{areas.map(area =>
<option key={area.id} value={area.id}>{area.name}</option>
)}
</HTMLSelect>
) : null}
{quest ? (
<Button
text="Save as..."
icon="floppy-disk"
onClick={this.onSaveAsClick}
/>
) : null}
</NavbarGroup>
</Navbar>
<div className="QuestEditorComponent-main">
<QuestInfoComponent quest={quest} />
<RendererComponent
quest={quest}
area={area}
model={model} />
<EntityInfoComponent entity={questEditorStore.selectedEntity} />
</div>
<Dialog
title="Save as..."
icon="floppy-disk"
className={Classes.DARK}
style={{ width: 360 }}
isOpen={this.state.saveDialogOpen}
onClose={this.onSaveDialogClose}>
<div className={Classes.DIALOG_BODY}>
<FormGroup label="Name:" labelFor="file-name-input">
<InputGroup
id="file-name-input"
autoFocus={true}
value={this.state.saveDialogFilename}
maxLength={12}
onChange={this.onSaveDialogNameChange}
onKeyUp={this.onSaveDialogNameKeyUp}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text="Save"
style={{ marginLeft: 10 }}
onClick={this.onSaveDialogSaveClick}
intent={Intent.PRIMARY} />
</div>
</div>
</Dialog>
</div>
);
}
private onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.files) {
const file = e.currentTarget.files[0];
if (file) {
this.setState({
filename: file.name
});
loadFile(file);
}
}
}
private onAreaSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
const areaId = parseInt(e.currentTarget.value, 10);
setCurrentAreaId(areaId);
}
private onSaveAsClick = () => {
let name = this.state.filename || 'Untitled';
name = name.endsWith('.qst') ? name.slice(0, -4) : name;
this.setState({
saveDialogOpen: true,
saveDialogFilename: name
});
}
private onSaveDialogNameChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ saveDialogFilename: e.currentTarget.value });
}
private onSaveDialogNameKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.onSaveDialogSaveClick();
}
}
private onSaveDialogSaveClick = () => {
saveCurrentQuestToFile(this.state.saveDialogFilename);
this.setState({ saveDialogOpen: false });
}
private onSaveDialogClose = () => {
this.setState({ saveDialogOpen: false });
}
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { NpcType, Quest } from '../domain'; import { NpcType, Quest } from '../../domain';
import { Pre } from '@blueprintjs/core'; import { Pre } from '@blueprintjs/core';
import './QuestInfoComponent.css'; import './QuestInfoComponent.css';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Object3D } from 'three'; import { Object3D } from 'three';
import { Area, Quest } from '../domain'; import { Area, Quest } from '../../domain';
import { Renderer } from '../rendering/Renderer'; import { Renderer } from '../../rendering/Renderer';
interface Props { interface Props {
quest?: Quest; quest?: Quest;