diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index 8de5c888..83ece4f2 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -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; } diff --git a/src/quest_editor/gui/EntityListView.ts b/src/quest_editor/gui/EntityListView.ts index bb74fec6..a34f6253 100644 --- a/src/quest_editor/gui/EntityListView.ts +++ b/src/quest_editor/gui/EntityListView.ts @@ -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 extends ResizableWidget { readonly element: HTMLElement; @@ -19,7 +18,7 @@ export abstract class EntityListView 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 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(`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(); - }, - }; -} diff --git a/src/quest_editor/gui/entity_dnd.ts b/src/quest_editor/gui/entity_dnd.ts new file mode 100644 index 00000000..0964e342 --- /dev/null +++ b/src/quest_editor/gui/entity_dnd.ts @@ -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 | 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; + } +} diff --git a/src/quest_editor/rendering/QuestEntityControls.ts b/src/quest_editor/rendering/QuestEntityControls.ts index fb32d0c6..f77ae267 100644 --- a/src/quest_editor/rendering/QuestEntityControls.ts +++ b/src/quest_editor/rendering/QuestEntityControls.ts @@ -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).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).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).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).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,