From 4bf2148b3963e688c7e294340f61b1cd5d8fda5d Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 29 Sep 2019 18:53:01 +0200 Subject: [PATCH] Entities can be rotated via the entity info view. --- FEATURES.md | 4 +- src/core/math.ts | 24 +++++ src/quest_editor/gui/EntityInfoView.ts | 107 ++++++++++++--------- src/quest_editor/model/QuestEntityModel.ts | 22 ++++- 4 files changed, 110 insertions(+), 47 deletions(-) create mode 100644 src/core/math.ts diff --git a/FEATURES.md b/FEATURES.md index ce3862ab..b1dda2e0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -66,7 +66,9 @@ Features that are in ***bold italics*** are planned and not yet implemented. - Translation - Via 3D view - Via entity view -- ***Rotation*** +- Rotation + - Via 3D view + - Via entity view - ***Multi select and translate/rotate/edit*** ## Events diff --git a/src/core/math.ts b/src/core/math.ts new file mode 100644 index 00000000..2f7910cf --- /dev/null +++ b/src/core/math.ts @@ -0,0 +1,24 @@ +const TO_DEG = 180 / Math.PI; +const TO_RAD = 1 / TO_DEG; + +/** + * Converts radians to degrees. + */ +export function rad_to_deg(rad: number): number { + return rad * TO_DEG; +} + +/** + * Converts degrees to radians. + */ +export function deg_to_rad(deg: number): number { + return deg * TO_RAD; +} + +/** + * @returns the floored modulus of its arguments. The computed value will have the same sign as the + * `divisor`. + */ +export function floor_mod(dividend: number, divisor: number): number { + return ((dividend % divisor) + divisor) % divisor; +} diff --git a/src/quest_editor/gui/EntityInfoView.ts b/src/quest_editor/gui/EntityInfoView.ts index d59cae6b..951f68aa 100644 --- a/src/quest_editor/gui/EntityInfoView.ts +++ b/src/quest_editor/gui/EntityInfoView.ts @@ -7,9 +7,9 @@ import { entity_data } from "../../core/data_formats/parsing/quest/entities"; import "./EntityInfoView.css"; import { NumberInput } from "../../core/gui/NumberInput"; import { Disposer } from "../../core/observable/Disposer"; -import { Property } from "../../core/observable/property/Property"; import { QuestEntityModel } from "../model/QuestEntityModel"; -import { Vector3 } from "three"; +import { Euler, Vector3 } from "three"; +import { deg_to_rad, rad_to_deg } from "../../core/math"; export class EntityInfoView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 }); @@ -30,13 +30,13 @@ export class EntityInfoView extends ResizableWidget { private readonly pos_z_element = this.disposable( new NumberInput(0, { width: 80, round_to: 3 }), ); - private readonly world_pos_x_element = this.disposable( + private readonly rot_x_element = this.disposable( new NumberInput(0, { width: 80, round_to: 3 }), ); - private readonly world_pos_y_element = this.disposable( + private readonly rot_y_element = this.disposable( new NumberInput(0, { width: 80, round_to: 3 }), ); - private readonly world_pos_z_element = this.disposable( + private readonly rot_z_element = this.disposable( new NumberInput(0, { width: 80, round_to: 3 }), ); @@ -53,7 +53,7 @@ export class EntityInfoView extends ResizableWidget { el.tr({}, el.th({ text: "Type:" }), (this.type_element = el.td())), el.tr({}, el.th({ text: "Name:" }), (this.name_element = el.td())), el.tr({}, el.th({ text: "Section:" }), (this.section_id_element = el.td())), - el.tr({}, el.th({ text: "Section position:", col_span: 2 })), + el.tr({}, el.th({ text: "Position:", col_span: 2 })), el.tr( {}, el.th({ text: "X:", class: coord_class }), @@ -69,21 +69,21 @@ export class EntityInfoView extends ResizableWidget { el.th({ text: "Z:", class: coord_class }), el.td({}, this.pos_z_element.element), ), - el.tr({}, el.th({ text: "World position:", col_span: 2 })), + el.tr({}, el.th({ text: "Rotation:", col_span: 2 })), el.tr( {}, el.th({ text: "X:", class: coord_class }), - el.td({}, this.world_pos_x_element.element), + el.td({}, this.rot_x_element.element), ), el.tr( {}, el.th({ text: "Y:", class: coord_class }), - el.td({}, this.world_pos_y_element.element), + el.td({}, this.rot_y_element.element), ), el.tr( {}, el.th({ text: "Z:", class: coord_class }), - el.td({}, this.world_pos_z_element.element), + el.td({}, this.rot_z_element.element), ), ); @@ -115,23 +115,7 @@ export class EntityInfoView extends ResizableWidget { ), ); - this.observe( - entity, - entity.position, - false, - this.pos_x_element, - this.pos_y_element, - this.pos_z_element, - ); - - this.observe( - entity, - entity.world_position, - true, - this.world_pos_x_element, - this.world_pos_y_element, - this.world_pos_z_element, - ); + this.observe_entity(entity); } }), ); @@ -144,54 +128,89 @@ export class EntityInfoView extends ResizableWidget { this.entity_disposer.dispose(); } - private observe( - entity: QuestEntityModel, - pos: Property, - world: boolean, - x_input: NumberInput, - y_input: NumberInput, - z_input: NumberInput, - ): void { + private observe_entity(entity: QuestEntityModel): void { + const pos = entity.position; + this.entity_disposer.add_all( pos.observe( ({ value: { x, y, z } }) => { - x_input.value.val = x; - y_input.value.val = y; - z_input.value.val = z; + this.pos_x_element.value.val = x; + this.pos_y_element.value.val = y; + this.pos_z_element.value.val = z; }, { call_now: true }, ), - x_input.value.observe(({ value }) => + this.pos_x_element.value.observe(({ value }) => quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, pos.val, new Vector3(value, pos.val.y, pos.val.z), - world, + false, ), ), - y_input.value.observe(({ value }) => + this.pos_y_element.value.observe(({ value }) => quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, pos.val, new Vector3(pos.val.x, value, pos.val.z), - world, + false, ), ), - z_input.value.observe(({ value }) => + this.pos_z_element.value.observe(({ value }) => quest_editor_store.translate_entity( entity, entity.section.val, entity.section.val, pos.val, new Vector3(pos.val.x, pos.val.y, value), - world, + false, + ), + ), + ); + + const rot = entity.rotation; + + this.entity_disposer.add_all( + rot.observe( + ({ value: { x, y, z } }) => { + this.rot_x_element.value.val = rad_to_deg(x); + this.rot_y_element.value.val = rad_to_deg(y); + this.rot_z_element.value.val = rad_to_deg(z); + }, + { call_now: true }, + ), + + this.rot_x_element.value.observe(({ value }) => + quest_editor_store.rotate_entity( + entity, + rot.val, + new Euler(deg_to_rad(value), rot.val.y, rot.val.z, "ZXY"), + false, + ), + ), + + this.rot_y_element.value.observe(({ value }) => + quest_editor_store.rotate_entity( + entity, + rot.val, + new Euler(rot.val.x, deg_to_rad(value), rot.val.z, "ZXY"), + false, + ), + ), + + this.rot_z_element.value.observe(({ value }) => + quest_editor_store.rotate_entity( + entity, + rot.val, + new Euler(rot.val.x, rot.val.y, deg_to_rad(value), "ZXY"), + false, ), ), ); diff --git a/src/quest_editor/model/QuestEntityModel.ts b/src/quest_editor/model/QuestEntityModel.ts index a55d7175..72b154c3 100644 --- a/src/quest_editor/model/QuestEntityModel.ts +++ b/src/quest_editor/model/QuestEntityModel.ts @@ -4,6 +4,7 @@ import { property } from "../../core/observable"; import { WritableProperty } from "../../core/observable/property/WritableProperty"; import { SectionModel } from "./SectionModel"; import { Euler, Quaternion, Vector3 } from "three"; +import { floor_mod } from "../../core/math"; const q1 = new Quaternion(); const q2 = new Quaternion(); @@ -120,6 +121,8 @@ export abstract class QuestEntityModel { } set_rotation(rot: Euler): this { + floor_mod_euler(rot); + this._rotation.val = rot; const section = this.section.val; @@ -127,7 +130,9 @@ export abstract class QuestEntityModel { if (section) { q1.setFromEuler(rot); q2.setFromEuler(section.rotation); - this._world_rotation.val = new Euler().setFromQuaternion(q1.multiply(q2), "ZXY"); + this._world_rotation.val = floor_mod_euler( + new Euler().setFromQuaternion(q1.multiply(q2), "ZXY"), + ); } else { this._world_rotation.val = rot; } @@ -136,6 +141,8 @@ export abstract class QuestEntityModel { } set_world_rotation(rot: Euler): this { + floor_mod_euler(rot); + this._world_rotation.val = rot; const section = this.section.val; @@ -144,7 +151,10 @@ export abstract class QuestEntityModel { q1.setFromEuler(rot); q2.setFromEuler(section.rotation); q2.inverse(); - this._rotation.val = new Euler().setFromQuaternion(q1.multiply(q2), "ZXY"); + + this._rotation.val = floor_mod_euler( + new Euler().setFromQuaternion(q1.multiply(q2), "ZXY"), + ); } else { this._rotation.val = rot; } @@ -152,3 +162,11 @@ export abstract class QuestEntityModel { return this; } } + +function floor_mod_euler(euler: Euler): Euler { + return euler.set( + floor_mod(euler.x, 2 * Math.PI), + floor_mod(euler.y, 2 * Math.PI), + floor_mod(euler.z, 2 * Math.PI), + ); +}