mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Entity drag and drop code doesn't use custom events anymore for increased performance and simplicity. Made code more typesafe and decoupled. Fixed some bugs.
This commit is contained in:
parent
4293a3862b
commit
6e666b0ea5
@ -72,10 +72,10 @@ export abstract class Renderer implements Disposable {
|
||||
this.schedule_render();
|
||||
}
|
||||
|
||||
pointer_pos_to_device_coords(v: Vector2): Vector2 {
|
||||
pointer_pos_to_device_coords(e: MouseEvent): Vector2 {
|
||||
const coords = this.renderer.getSize(new Vector2());
|
||||
coords.width = (v.x / coords.width) * 2 - 1;
|
||||
coords.height = (v.y / coords.height) * -2 + 1;
|
||||
coords.width = (e.offsetX / coords.width) * 2 - 1;
|
||||
coords.height = (e.offsetY / coords.height) * -2 + 1;
|
||||
return coords;
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,7 @@ import { bind_children_to, el } from "../../core/gui/dom";
|
||||
import "./EntityListView.css";
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { ListProperty } from "../../core/observable/property/list/ListProperty";
|
||||
import { Vec2 } from "../../core/data_formats/vector";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { entity_dnd_source } from "./entity_dnd";
|
||||
|
||||
export abstract class EntityListView<T extends EntityType> extends ResizableWidget {
|
||||
readonly element: HTMLElement;
|
||||
@ -19,7 +18,7 @@ export abstract class EntityListView<T extends EntityType> extends ResizableWidg
|
||||
this.disposables(
|
||||
bind_children_to(list_element, entities, this.create_entity_element),
|
||||
|
||||
make_draggable(list_element, target => {
|
||||
entity_dnd_source(list_element, target => {
|
||||
if (target !== list_element) {
|
||||
const drag_element = target.cloneNode(true) as HTMLElement;
|
||||
drag_element.style.width = "100px";
|
||||
@ -43,119 +42,3 @@ export abstract class EntityListView<T extends EntityType> extends ResizableWidg
|
||||
return div;
|
||||
};
|
||||
}
|
||||
|
||||
export type EntityDrag = {
|
||||
readonly offset_x: number;
|
||||
readonly offset_y: number;
|
||||
readonly data_transfer: DataTransfer;
|
||||
readonly drag_element: HTMLElement;
|
||||
readonly entity_type: EntityType;
|
||||
};
|
||||
|
||||
function make_draggable(
|
||||
element: HTMLElement,
|
||||
start: (target: HTMLElement) => [HTMLElement, any] | undefined,
|
||||
): Disposable {
|
||||
let detail: { drag_element: HTMLElement; entity_type: EntityType } | undefined;
|
||||
const grab_point = new Vec2(0, 0);
|
||||
|
||||
function clear(): void {
|
||||
if (detail) {
|
||||
detail.drag_element.remove();
|
||||
detail = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function redispatch(e: DragEvent): void {
|
||||
if (e.target instanceof HTMLElement && detail && e.dataTransfer) {
|
||||
e.target.dispatchEvent(
|
||||
new CustomEvent<EntityDrag>(`phantasmal-${e.type}`, {
|
||||
detail: {
|
||||
...detail,
|
||||
data_transfer: e.dataTransfer,
|
||||
offset_x: e.offsetX,
|
||||
offset_y: e.offsetY,
|
||||
},
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function dragstart(e: DragEvent): void {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
clear();
|
||||
|
||||
const result = start(e.target);
|
||||
|
||||
if (result) {
|
||||
grab_point.set(e.offsetX + 2, e.offsetY + 2);
|
||||
|
||||
detail = {
|
||||
drag_element: result[0],
|
||||
entity_type: result[1],
|
||||
};
|
||||
|
||||
detail.drag_element.style.position = "fixed";
|
||||
detail.drag_element.style.pointerEvents = "none";
|
||||
detail.drag_element.style.zIndex = "500";
|
||||
detail.drag_element.style.top = "0";
|
||||
detail.drag_element.style.left = "0";
|
||||
detail.drag_element.style.transform = `translate(${e.clientX -
|
||||
grab_point.x}px, ${e.clientY - grab_point.y}px)`;
|
||||
document.body.append(detail.drag_element);
|
||||
|
||||
e.dataTransfer!.setDragImage(el.div(), 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dragenter(e: DragEvent): void {
|
||||
redispatch(e);
|
||||
}
|
||||
|
||||
function dragover(e: DragEvent): void {
|
||||
if (e.target instanceof HTMLElement && detail) {
|
||||
detail.drag_element.style.transform = `translate(${e.clientX -
|
||||
grab_point.x}px, ${e.clientY - grab_point.y}px)`;
|
||||
|
||||
redispatch(e);
|
||||
}
|
||||
}
|
||||
|
||||
function dragleave(e: DragEvent): void {
|
||||
redispatch(e);
|
||||
}
|
||||
|
||||
function dragend(): void {
|
||||
clear();
|
||||
}
|
||||
|
||||
function drop(e: DragEvent): void {
|
||||
try {
|
||||
redispatch(e);
|
||||
} finally {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("dragstart", dragstart);
|
||||
document.addEventListener("dragenter", dragenter);
|
||||
document.addEventListener("dragover", dragover);
|
||||
document.addEventListener("dragleave", dragleave);
|
||||
document.addEventListener("dragend", dragend);
|
||||
document.addEventListener("drop", drop);
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
element.removeEventListener("dragstart", dragstart);
|
||||
document.removeEventListener("dragenter", dragenter);
|
||||
document.removeEventListener("dragover", dragover);
|
||||
document.removeEventListener("dragleave", dragleave);
|
||||
document.removeEventListener("dragend", dragend);
|
||||
document.removeEventListener("drop", drop);
|
||||
|
||||
clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
120
src/quest_editor/gui/entity_dnd.ts
Normal file
120
src/quest_editor/gui/entity_dnd.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { Vec2 } from "../../core/data_formats/vector";
|
||||
import { el } from "../../core/gui/dom";
|
||||
|
||||
export type EntityDragEvent = {
|
||||
readonly entity_type: EntityType;
|
||||
readonly drag_element: HTMLElement;
|
||||
readonly event: DragEvent;
|
||||
};
|
||||
|
||||
let dragging_details: Omit<EntityDragEvent, "event"> | undefined = undefined;
|
||||
const listeners: Map<(e: EntityDragEvent) => void, (e: DragEvent) => void> = new Map();
|
||||
const grab_point = new Vec2(0, 0);
|
||||
let drag_sources = 0;
|
||||
|
||||
export function add_entity_dnd_listener(
|
||||
element: HTMLElement,
|
||||
type: "dragenter" | "dragover" | "dragleave" | "drop",
|
||||
listener: (event: EntityDragEvent) => void,
|
||||
): void {
|
||||
function event_listener(event: DragEvent): void {
|
||||
if (dragging_details) {
|
||||
listener({ ...dragging_details, event });
|
||||
}
|
||||
}
|
||||
|
||||
listeners.set(listener, event_listener);
|
||||
element.addEventListener(type, event_listener);
|
||||
}
|
||||
|
||||
export function remove_entity_dnd_listener(
|
||||
element: HTMLElement,
|
||||
type: "dragenter" | "dragover" | "dragleave" | "drop",
|
||||
listener: (event: EntityDragEvent) => void,
|
||||
): void {
|
||||
const event_listener = listeners.get(listener);
|
||||
|
||||
if (event_listener) {
|
||||
listeners.delete(listener);
|
||||
element.removeEventListener(type, event_listener);
|
||||
}
|
||||
}
|
||||
|
||||
export function entity_dnd_source(
|
||||
element: HTMLElement,
|
||||
start: (target: HTMLElement) => [HTMLElement, EntityType] | undefined,
|
||||
): Disposable {
|
||||
function dragstart(e: DragEvent): void {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
const result = start(e.target);
|
||||
|
||||
if (result) {
|
||||
grab_point.set(e.offsetX + 2, e.offsetY + 2);
|
||||
|
||||
dragging_details = {
|
||||
drag_element: result[0],
|
||||
entity_type: result[1],
|
||||
};
|
||||
|
||||
dragging_details.drag_element.style.position = "fixed";
|
||||
dragging_details.drag_element.style.pointerEvents = "none";
|
||||
dragging_details.drag_element.style.zIndex = "500";
|
||||
dragging_details.drag_element.style.top = "0";
|
||||
dragging_details.drag_element.style.left = "0";
|
||||
dragging_details.drag_element.style.transform = `translate(${e.clientX -
|
||||
grab_point.x}px, ${e.clientY - grab_point.y}px)`;
|
||||
document.body.append(dragging_details.drag_element);
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = "copy";
|
||||
e.dataTransfer.setDragImage(el.div(), 0, 0);
|
||||
// setData is necessary for FireFox.
|
||||
e.dataTransfer.setData(
|
||||
"phantasmal-entity",
|
||||
entity_data(dragging_details.entity_type).name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("dragstart", dragstart);
|
||||
|
||||
if (++drag_sources === 1) {
|
||||
document.addEventListener("dragover", dragover);
|
||||
document.addEventListener("dragend", dragend);
|
||||
}
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
element.removeEventListener("dragstart", dragstart);
|
||||
|
||||
if (--drag_sources === 0) {
|
||||
document.removeEventListener("dragover", dragover);
|
||||
document.removeEventListener("dragend", dragend);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function dragover(e: DragEvent): void {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = "none";
|
||||
}
|
||||
|
||||
if (dragging_details) {
|
||||
dragging_details.drag_element.style.transform = `translate(${e.clientX -
|
||||
grab_point.x}px, ${e.clientY - grab_point.y}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
function dragend(): void {
|
||||
if (dragging_details) {
|
||||
dragging_details.drag_element.remove();
|
||||
dragging_details = undefined;
|
||||
}
|
||||
}
|
@ -9,9 +9,13 @@ import { AreaUserData } from "./conversion/areas";
|
||||
import { SectionModel } from "../model/SectionModel";
|
||||
import { Disposable } from "../../core/observable/Disposable";
|
||||
import { Disposer } from "../../core/observable/Disposer";
|
||||
import { EntityDrag } from "../gui/EntityListView";
|
||||
import { is_npc_type } from "../../core/data_formats/parsing/quest/entities";
|
||||
import { npc_data } from "../../core/data_formats/parsing/quest/npc_types";
|
||||
import {
|
||||
add_entity_dnd_listener,
|
||||
EntityDragEvent,
|
||||
remove_entity_dnd_listener,
|
||||
} from "../gui/entity_dnd";
|
||||
|
||||
const DOWN_VECTOR = new Vector3(0, -1, 0);
|
||||
|
||||
@ -20,11 +24,13 @@ type Highlighted = {
|
||||
mesh: Mesh;
|
||||
};
|
||||
|
||||
enum PickMode {
|
||||
Creating,
|
||||
Transforming,
|
||||
}
|
||||
|
||||
type Pick = {
|
||||
/**
|
||||
* Whether we picked an entity that is being created or one that has existed before.
|
||||
*/
|
||||
creating: boolean;
|
||||
mode: PickMode;
|
||||
|
||||
initial_section?: SectionModel;
|
||||
|
||||
@ -82,12 +88,20 @@ export class QuestEntityControls implements Disposable {
|
||||
renderer.dom_element.addEventListener("mousedown", this.mousedown);
|
||||
renderer.dom_element.addEventListener("mousemove", this.mousemove);
|
||||
renderer.dom_element.addEventListener("mouseup", this.mouseup);
|
||||
renderer.dom_element.addEventListener("phantasmal-dragenter", this.dragenter);
|
||||
renderer.dom_element.addEventListener("phantasmal-dragover", this.dragover);
|
||||
renderer.dom_element.addEventListener("phantasmal-dragleave", this.dragleave);
|
||||
add_entity_dnd_listener(renderer.dom_element, "dragenter", this.dragenter);
|
||||
add_entity_dnd_listener(renderer.dom_element, "dragover", this.dragover);
|
||||
add_entity_dnd_listener(renderer.dom_element, "dragleave", this.dragleave);
|
||||
add_entity_dnd_listener(renderer.dom_element, "drop", this.drop);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.renderer.dom_element.removeEventListener("mousedown", this.mousedown);
|
||||
this.renderer.dom_element.removeEventListener("mousemove", this.mousemove);
|
||||
this.renderer.dom_element.removeEventListener("mouseup", this.mouseup);
|
||||
remove_entity_dnd_listener(this.renderer.dom_element, "dragenter", this.dragenter);
|
||||
remove_entity_dnd_listener(this.renderer.dom_element, "dragover", this.dragover);
|
||||
remove_entity_dnd_listener(this.renderer.dom_element, "dragleave", this.dragleave);
|
||||
remove_entity_dnd_listener(this.renderer.dom_element, "drop", this.drop);
|
||||
this.disposer.dispose();
|
||||
}
|
||||
|
||||
@ -112,9 +126,7 @@ export class QuestEntityControls implements Disposable {
|
||||
this.process_event(e);
|
||||
this.stop_transforming();
|
||||
|
||||
const new_pick = this.pick_entity(
|
||||
this.renderer.pointer_pos_to_device_coords(new Vector2(e.offsetX, e.offsetY)),
|
||||
);
|
||||
const new_pick = this.pick_entity(this.renderer.pointer_pos_to_device_coords(e));
|
||||
|
||||
if (new_pick) {
|
||||
// Disable camera controls while the user is transforming an entity.
|
||||
@ -132,9 +144,7 @@ export class QuestEntityControls implements Disposable {
|
||||
private mousemove = (e: MouseEvent) => {
|
||||
this.process_event(e);
|
||||
|
||||
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(
|
||||
new Vector2(e.offsetX, e.offsetY),
|
||||
);
|
||||
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e);
|
||||
|
||||
if (this.selected && this.pick) {
|
||||
if (this.moved_since_last_mouse_down) {
|
||||
@ -177,16 +187,11 @@ export class QuestEntityControls implements Disposable {
|
||||
this.renderer.schedule_render();
|
||||
};
|
||||
|
||||
private dragenter = (e: Event) => {
|
||||
private dragenter = (e: EntityDragEvent) => {
|
||||
const area = quest_editor_store.current_area.val;
|
||||
if (!area) return;
|
||||
|
||||
const detail = (e as CustomEvent<EntityDrag>).detail;
|
||||
|
||||
const pointer_position = this.renderer.pointer_pos_to_device_coords(
|
||||
new Vector2(detail.offset_x, detail.offset_y),
|
||||
);
|
||||
|
||||
const pointer_position = this.renderer.pointer_pos_to_device_coords(e.event);
|
||||
const { intersection, section } = this.pick_terrain(pointer_position, new Vector3());
|
||||
|
||||
let position: Vec3 | undefined;
|
||||
@ -208,15 +213,18 @@ export class QuestEntityControls implements Disposable {
|
||||
const quest = quest_editor_store.current_quest.val;
|
||||
|
||||
if (quest && position) {
|
||||
if (is_npc_type(detail.entity_type)) {
|
||||
const data = npc_data(detail.entity_type);
|
||||
if (is_npc_type(e.entity_type)) {
|
||||
const data = npc_data(e.entity_type);
|
||||
|
||||
if (data.pso_type_id !== undefined && data.pso_roaming != undefined) {
|
||||
detail.drag_element.style.display = "none";
|
||||
detail.data_transfer.dropEffect = "copy";
|
||||
if (data.pso_type_id != undefined && data.pso_roaming != undefined) {
|
||||
e.drag_element.style.display = "none";
|
||||
|
||||
if (e.event.dataTransfer) {
|
||||
e.event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
const npc = new QuestNpcModel(
|
||||
detail.entity_type,
|
||||
e.entity_type,
|
||||
data.pso_type_id,
|
||||
0,
|
||||
0,
|
||||
@ -235,7 +243,7 @@ export class QuestEntityControls implements Disposable {
|
||||
quest_editor_store.set_selected_entity(npc);
|
||||
|
||||
this.pick = {
|
||||
creating: true,
|
||||
mode: PickMode.Creating,
|
||||
initial_section: section,
|
||||
initial_position: position,
|
||||
grab_offset: new Vector3(0, 0, 0),
|
||||
@ -247,83 +255,63 @@ export class QuestEntityControls implements Disposable {
|
||||
}
|
||||
};
|
||||
|
||||
private dragover = (e: Event) => {
|
||||
private dragover = (e: EntityDragEvent) => {
|
||||
if (!quest_editor_store.current_area.val) return;
|
||||
|
||||
const detail = (e as CustomEvent<EntityDrag>).detail;
|
||||
detail.data_transfer.dropEffect = "copy";
|
||||
if (this.pick && this.pick.mode === PickMode.Creating) {
|
||||
e.event.stopPropagation();
|
||||
e.event.preventDefault();
|
||||
|
||||
if (this.selected && this.pick) {
|
||||
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(
|
||||
new Vector2(detail.offset_x, detail.offset_y),
|
||||
);
|
||||
if (e.event.dataTransfer) {
|
||||
e.event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// if (e.buttons === 1) {
|
||||
// User is transforming selected entity.
|
||||
// User is dragging selected entity.
|
||||
// if (e.shiftKey) {
|
||||
// Vertical movement.
|
||||
// this.translate_vertically(this.selected, this.pick, pointer_device_pos);
|
||||
// } else {
|
||||
// Horizontal movement across terrain.
|
||||
this.translate_horizontally(this.selected, this.pick, pointer_device_pos);
|
||||
// }
|
||||
// }
|
||||
if (this.selected) {
|
||||
const pointer_device_pos = this.renderer.pointer_pos_to_device_coords(e.event);
|
||||
|
||||
this.renderer.schedule_render();
|
||||
if (e.event.shiftKey) {
|
||||
// Vertical movement.
|
||||
this.translate_vertically(this.selected, this.pick, pointer_device_pos);
|
||||
} else {
|
||||
// Horizontal movement across terrain.
|
||||
this.translate_horizontally(this.selected, this.pick, pointer_device_pos);
|
||||
}
|
||||
|
||||
this.renderer.schedule_render();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private dragleave = (e: Event) => {
|
||||
private dragleave = (e: EntityDragEvent) => {
|
||||
if (!quest_editor_store.current_area.val) return;
|
||||
|
||||
const detail = (e as CustomEvent<EntityDrag>).detail;
|
||||
if (detail.drag_element) detail.drag_element.style.display = "flex";
|
||||
e.drag_element.style.display = "flex";
|
||||
|
||||
const quest = quest_editor_store.current_quest.val;
|
||||
|
||||
if (quest && this.selected && this.selected.entity.type == detail.entity_type) {
|
||||
if (quest && this.selected && this.pick && this.pick.mode === PickMode.Creating) {
|
||||
quest.remove_entity(this.selected.entity);
|
||||
}
|
||||
};
|
||||
|
||||
private drop = () => {
|
||||
// TODO: push onto undo stack.
|
||||
this.pick = undefined;
|
||||
};
|
||||
|
||||
private process_event(e: Event): void {
|
||||
let offset_x: number;
|
||||
let offset_y: number;
|
||||
|
||||
if (e instanceof MouseEvent) {
|
||||
offset_x = e.offsetX;
|
||||
offset_y = e.offsetY;
|
||||
} else if (e instanceof CustomEvent) {
|
||||
const detail = (e as CustomEvent<EntityDrag>).detail;
|
||||
|
||||
if (!("offset_x" in detail && "offset_y" in detail)) {
|
||||
return;
|
||||
}
|
||||
|
||||
offset_x = detail.offset_x;
|
||||
offset_y = detail.offset_y;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.type === "mousedown" || e.type === "phantasmal-dragenter") {
|
||||
private process_event(e: MouseEvent): void {
|
||||
if (e.type === "mousedown") {
|
||||
this.moved_since_last_mouse_down = false;
|
||||
} else {
|
||||
if (
|
||||
offset_x !== this.last_pointer_position.x ||
|
||||
offset_y !== this.last_pointer_position.y
|
||||
e.offsetX !== this.last_pointer_position.x ||
|
||||
e.offsetY !== this.last_pointer_position.y
|
||||
) {
|
||||
this.moved_since_last_mouse_down = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.last_pointer_position.set(offset_x, offset_y);
|
||||
this.last_pointer_position.set(e.offsetX, e.offsetY);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -456,7 +444,12 @@ export class QuestEntityControls implements Disposable {
|
||||
}
|
||||
|
||||
private stop_transforming = () => {
|
||||
if (this.moved_since_last_mouse_down && this.selected && this.pick && !this.pick.creating) {
|
||||
if (
|
||||
this.moved_since_last_mouse_down &&
|
||||
this.selected &&
|
||||
this.pick &&
|
||||
this.pick.mode === PickMode.Transforming
|
||||
) {
|
||||
const entity = this.selected.entity;
|
||||
quest_editor_store.push_translate_entity_action(
|
||||
entity,
|
||||
@ -503,7 +496,7 @@ export class QuestEntityControls implements Disposable {
|
||||
}
|
||||
|
||||
return {
|
||||
creating: false,
|
||||
mode: PickMode.Transforming,
|
||||
mesh: intersection.object as Mesh,
|
||||
entity,
|
||||
initial_section: entity.section.val,
|
||||
|
Loading…
Reference in New Issue
Block a user