diff --git a/app/pages/PageEnigma/Editor/Commands.ts b/app/pages/PageEnigma/Editor/Commands.ts new file mode 100644 index 0000000000..50bbe60715 --- /dev/null +++ b/app/pages/PageEnigma/Editor/Commands.ts @@ -0,0 +1,169 @@ +import { SceneManager } from "./scene_manager_api"; +import * as THREE from "three"; + +export interface ICommand { + undo(): Promise; + redo(): Promise; +} + +export interface TransformData { + object_uuid: string; + position: THREE.Vector3; + rotation: THREE.Euler; + scale: THREE.Vector3; +} + +export interface UserData { + object_uuid: string; + color: string; + visible: boolean; + locked: boolean; +} + +export interface CreationData { + object_uuid: string; + name: string; + position: THREE.Vector3; + userdata: Record; +} + +export interface DeletionData { + object_uuid: string; + name: string; + position: THREE.Vector3; + userdata: Record; +} + +export interface SceneState { + scene_objects: string[]; + scene_names: string[]; + scene_userdata: string[]; + scene_positions: string[]; + scene_rotations: string[]; + scene_scales: string[]; +} + +export type CommandInputTypes = TransformData | CreationData | DeletionData; + +export class CreationSceneItem implements ICommand { + private component: SceneManager; + private creationData: CreationData; + + public constructor(sceneManager: SceneManager, creationData: CreationData) { + this.component = sceneManager; + this.creationData = creationData; + } + + public async undo(): Promise { + this.component.delete(this.creationData.object_uuid); + } + + public async redo(): Promise { + const object = await this.component.create( + this.creationData.userdata["media_id"], + this.creationData.name, + this.creationData.position, + ); + object.position.copy(this.creationData.position); + object.uuid = this.creationData.object_uuid; + if (this.creationData.userdata["visible"] !== undefined) { + object.visible = this.creationData.userdata["visible"]; + } + this.component.scene.setColor( + object.uuid, + this.creationData.userdata["color"], + ); + } +} + +export class DeletionSceneItem implements ICommand { + private component: SceneManager; + private deletionData: DeletionData; + + public constructor(sceneManager: SceneManager, creationData: DeletionData) { + this.component = sceneManager; + this.deletionData = creationData; + } + + public async undo(): Promise { + const object = await this.component.create( + this.deletionData.userdata["media_id"], + this.deletionData.name, + this.deletionData.position, + ); + object.uuid = this.deletionData.object_uuid; + object.visible = this.deletionData.userdata["visible"]; + this.component.scene.setColor( + object.uuid, + this.deletionData.userdata["color"], + ); + } + + public async redo(): Promise { + this.component.delete(this.deletionData.object_uuid); + } +} + +export class TransformSceneItem implements ICommand { + private component: SceneManager; + private transformDataStart: TransformData; + private transformDataEnd: TransformData; + + public constructor( + sceneManager: SceneManager, + transformDataStart: TransformData, + transformDataEnd: TransformData, + ) { + this.component = sceneManager; + this.transformDataStart = transformDataStart; + this.transformDataEnd = transformDataEnd; + } + + public async undo(): Promise { + this.component.update( + this.transformDataStart.object_uuid, + this.transformDataStart.position, + this.transformDataStart.rotation, + this.transformDataStart.scale, + ); + } + + public async redo(): Promise { + this.component.update( + this.transformDataEnd.object_uuid, + this.transformDataEnd.position, + this.transformDataEnd.rotation, + this.transformDataEnd.scale, + ); + } +} + +export class UserDataSceneItem implements ICommand { + private component: SceneManager; + private startUserData: UserData; + private endUserData: UserData; + + public constructor( + sceneManager: SceneManager, + startUserData: UserData, + endUserData: UserData, + ) { + this.component = sceneManager; + this.startUserData = startUserData; + this.endUserData = endUserData; + } + + public async undo(): Promise { + this.component.scene.setColor( + this.startUserData.object_uuid, + this.startUserData.color, + ); + } + + public async redo(): Promise { + this.component.scene.setColor( + this.endUserData.object_uuid, + this.endUserData.color, + ); + } +} diff --git a/app/pages/PageEnigma/Editor/editor.ts b/app/pages/PageEnigma/Editor/editor.ts index d86df165a5..1eb0f83bfe 100644 --- a/app/pages/PageEnigma/Editor/editor.ts +++ b/app/pages/PageEnigma/Editor/editor.ts @@ -493,9 +493,7 @@ class Editor { this.cam_obj = this.activeScene.get_object_by_name(this.camera_name); if (this.outliner_feature_flag) { - const result = this.sceneManager?.render_outliner( - this.timeline.characters, - ); + const result = this.sceneManager?.render_outliner(this.timeline.characters); if (result) outlinerState.items.value = result.items; } @@ -531,6 +529,7 @@ class Editor { this.mouse_controls, this.activeScene, true, + this.updateOutliner.bind(this), ); // Enabled dev mode. this.mouse_controls.sceneManager = this.sceneManager; } @@ -1029,6 +1028,12 @@ class Editor { } } + updateOutliner() { + const result = this.sceneManager?.render_outliner(this.timeline.characters); + if (result) outlinerState.items.value = result.items; + this.updateSelectedUI(); + } + updateSelectedUI() { let mainSelected; if (this.outliner_feature_flag) { diff --git a/app/pages/PageEnigma/Editor/keybinds_controls.ts b/app/pages/PageEnigma/Editor/keybinds_controls.ts index e638581f70..d8b9672611 100644 --- a/app/pages/PageEnigma/Editor/keybinds_controls.ts +++ b/app/pages/PageEnigma/Editor/keybinds_controls.ts @@ -1,261 +1,302 @@ import * as THREE from "three"; -import { hideObjectPanel, hotkeysStatus, outlinerState, showObjectPanel } from "../signals"; -import { OrbitControls, OutlinePass, PointerLockControls } from "three/examples/jsm/Addons.js"; +import { + hideObjectPanel, + hotkeysStatus, + outlinerState, + showObjectPanel, +} from "../signals"; +import { + OrbitControls, + OutlinePass, + PointerLockControls, +} from "three/examples/jsm/Addons.js"; import { TransformControls } from "./TransformControls"; import { SceneManager, SceneObject } from "./scene_manager_api"; export class MouseControls { - camera: THREE.PerspectiveCamera | null; - get_camera_person_mode: Function; - lockControls: PointerLockControls | undefined; - camera_last_pos: THREE.Vector3; - selected: THREE.Object3D[] | undefined; - orbitControls: OrbitControls | undefined; - selectedCanvas: boolean; - switchPreviewToggle: boolean; - rendering: boolean; - togglePlayback: Function; - deleteObject: Function; - canvReference: HTMLCanvasElement | null = null; - mouse: THREE.Vector2 | undefined; - timeline_mouse: THREE.Vector2 | undefined; - control: TransformControls | undefined; - raycaster: THREE.Raycaster | undefined; - outlinePass: OutlinePass | undefined; - scene: THREE.Scene; - publishSelect: Function; - updateSelectedUI: Function; - transform_interaction: boolean; - last_selected: THREE.Object3D[] | undefined; - getAssetType: Function; - setSelected: Function; - sceneManager: SceneManager | undefined; + camera: THREE.PerspectiveCamera | null; + camera_person_mode: boolean; + lockControls: PointerLockControls | undefined; + camera_last_pos: THREE.Vector3; + selected: THREE.Object3D[] | undefined; + orbitControls: OrbitControls | undefined; + selectedCanvas: boolean; + switchPreviewToggle: boolean; + rendering: boolean; + togglePlayback: Function; + deleteObject: Function; + canvReference: HTMLCanvasElement | null = null; + mouse: THREE.Vector2 | undefined; + timeline_mouse: THREE.Vector2 | undefined; + control: TransformControls | undefined; + raycaster: THREE.Raycaster | undefined; + outlinePass: OutlinePass | undefined; + scene: THREE.Scene; + publishSelect: Function; + updateSelectedUI: Function; + transform_interaction: boolean; + last_selected: THREE.Object3D[] | undefined; + getAssetType: Function; + setSelected: Function; + sceneManager: SceneManager | undefined; + private isProcessing: boolean = false; - constructor(camera: THREE.PerspectiveCamera | null, get_camera_person_mode: Function, lockControls: PointerLockControls | undefined, - camera_last_pos: THREE.Vector3, orbitControls: OrbitControls | undefined, - selectedCanvas: boolean, switchPreviewToggle: boolean, rendering: boolean, togglePlayback: Function, - deleteObject: Function, canvReference: HTMLCanvasElement | null, - mouse: THREE.Vector2 | undefined, timeline_mouse: THREE.Vector2 | undefined, - raycaster: THREE.Raycaster | undefined, control: TransformControls | undefined, - outlinePass: OutlinePass | undefined, scene: THREE.Scene, - publishSelect: Function, updateSelectedUI: Function, - transform_interaction: boolean, last_selected: THREE.Object3D | undefined, - getAssetType: Function, setSelected: Function - ) { - this.camera = camera; - this.get_camera_person_mode = get_camera_person_mode; - this.lockControls = lockControls; - this.camera_last_pos = camera_last_pos; - this.selected = []; - this.orbitControls = orbitControls; - this.selectedCanvas = selectedCanvas; - this.switchPreviewToggle = switchPreviewToggle; - this.rendering = rendering; - this.togglePlayback = togglePlayback; - this.deleteObject = deleteObject; - this.canvReference = canvReference; - this.mouse = mouse; - this.timeline_mouse = timeline_mouse; - this.raycaster = raycaster; - this.control = control; - this.outlinePass = outlinePass; - this.scene = scene; - this.publishSelect = publishSelect; - this.updateSelectedUI = updateSelectedUI; - this.transform_interaction = transform_interaction; - this.last_selected = []; - this.getAssetType = getAssetType; - this.setSelected = setSelected; - this.sceneManager = undefined; + constructor( + camera: THREE.PerspectiveCamera | null, + camera_person_mode: boolean, + lockControls: PointerLockControls | undefined, + camera_last_pos: THREE.Vector3, + orbitControls: OrbitControls | undefined, + selectedCanvas: boolean, + switchPreviewToggle: boolean, + rendering: boolean, + togglePlayback: Function, + deleteObject: Function, + canvReference: HTMLCanvasElement | null, + mouse: THREE.Vector2 | undefined, + timeline_mouse: THREE.Vector2 | undefined, + raycaster: THREE.Raycaster | undefined, + control: TransformControls | undefined, + outlinePass: OutlinePass | undefined, + scene: THREE.Scene, + publishSelect: Function, + updateSelectedUI: Function, + transform_interaction: boolean, + last_selected: THREE.Object3D | undefined, + getAssetType: Function, + setSelected: Function, + ) { + this.camera = camera; + this.camera_person_mode = camera_person_mode; + this.lockControls = lockControls; + this.camera_last_pos = camera_last_pos; + this.selected = []; + this.orbitControls = orbitControls; + this.selectedCanvas = selectedCanvas; + this.switchPreviewToggle = switchPreviewToggle; + this.rendering = rendering; + this.togglePlayback = togglePlayback; + this.deleteObject = deleteObject; + this.canvReference = canvReference; + this.mouse = mouse; + this.timeline_mouse = timeline_mouse; + this.raycaster = raycaster; + this.control = control; + this.outlinePass = outlinePass; + this.scene = scene; + this.publishSelect = publishSelect; + this.updateSelectedUI = updateSelectedUI; + this.transform_interaction = transform_interaction; + this.last_selected = []; + this.getAssetType = getAssetType; + this.setSelected = setSelected; + this.sceneManager = undefined; + } + + focus() { + if (this.orbitControls && this.selected) { + this.orbitControls.target.copy(this.selected[0].position); + this.orbitControls.maxDistance = 4; + this.orbitControls.update(); + this.orbitControls.maxDistance = 999; + } + } + + removeTransformControls(remove_outline: boolean = true) { + if (this.control == undefined) { + return; + } + if (this.outlinePass == undefined) { + return; + } + if (remove_outline) { + this.last_selected = this.selected; + this.selected = []; + this.publishSelect(); + } + this.control.detach(); + this.scene.remove(this.control); + if (remove_outline) this.outlinePass.selectedObjects = []; + } + + selectObject(currentObject: THREE.Object3D) { + this.setSelected(this.selected); + this.publishSelect(); + + // this.editor.update_properties() + if (currentObject.userData["locked"] !== true && this.control) { + this.scene.add(this.control); + this.control.attach(currentObject); } - focus() { - if(this.orbitControls){ - this.orbitControls.target.copy(this.selected[0].position); - this.orbitControls.maxDistance = 4; - this.orbitControls.update(); - this.orbitControls.maxDistance = 999; - } + if (this.selected && this.outlinePass) { + this.outlinePass.selectedObjects = this.selected; + } + this.transform_interaction = true; + // Contact react land + showObjectPanel(); + this.updateSelectedUI(); + } + + onMouseDown(event: any) { + if (event.button === 1 && this.camera_person_mode) { + this.lockControls?.lock(); + } + } + + onMouseUp(event: any) { + if (event.button === 1) { + this.lockControls?.unlock(); } - removeTransformControls(remove_outline: boolean = true) { - if (this.control == undefined) { - return; - } - if (this.outlinePass == undefined) { - return; - } - if (remove_outline) { - this.last_selected = this.selected; - this.selected = []; - this.publishSelect(); - } - this.control.detach(); - this.scene.remove(this.control); - if (remove_outline) this.outlinePass.selectedObjects = []; + if (event.button !== 0 && this.camera) { + const camera_pos = new THREE.Vector3( + parseFloat(this.camera.position.x.toFixed(2)), + parseFloat(this.camera.position.y.toFixed(2)), + parseFloat(this.camera.position.z.toFixed(2)), + ); + this.camera_last_pos.copy(camera_pos); } + } - selectObject(currentObject: THREE.Object3D) { - this.setSelected(this.selected); - this.publishSelect(); + sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } - // this.editor.update_properties() - if (currentObject.userData["locked"] !== true && this.control) { - this.scene.add(this.control); - this.control.attach(currentObject); - } - - if(this.selected && this.outlinePass){ - this.outlinePass.selectedObjects = this.selected; - } - this.transform_interaction = true; - // Contact react land - showObjectPanel(); - this.updateSelectedUI(); - } - - onMouseDown(event: any) { - if (event.button === 1 && this.get_camera_person_mode()) { - this.lockControls?.lock(); - console.log("LOCK") - } - } - - onMouseUp(event: any) { - if (event.button === 1) { - this.lockControls?.unlock(); - console.log("UNLOCK") - - } - - if (event.button !== 0 && this.camera) { - const camera_pos = new THREE.Vector3( - parseFloat(this.camera.position.x.toFixed(2)), - parseFloat(this.camera.position.y.toFixed(2)), - parseFloat(this.camera.position.z.toFixed(2)), - ); - this.camera_last_pos.copy(camera_pos); - } - } - - onkeydown(event: KeyboardEvent) { - if (hotkeysStatus.value.disabled) { - return; - } - else if (event.key === "f" && this.selected && this.orbitControls) { - this.focus(); - return; - } - else if (event.key === " ") { - if (!this.rendering && !this.switchPreviewToggle && this.selectedCanvas) { - this.togglePlayback(); - } - return; - } - else if (event.key === "Backspace" || event.key === "Delete") { - if (this.selected) { - this.selected.forEach(selected => { - this.deleteObject(selected.uuid); - }); - } - return; - } - else if (event.key === "t") { // transform - this.control?.setMode("translate"); - return; - } - else if (event.key === "r") { // rotate - this.control?.setMode("rotate"); - return; - } - else if (event.key === "g") { // scale - this.control?.setMode("scale"); - return; - } - } - - // Sets new mouse location usually used in raycasts. - onMouseMove(event: MouseEvent) { - if (this.canvReference == undefined) { return; } - const rect = this.canvReference.getBoundingClientRect(); - if (this.mouse == undefined) { - return; - } - this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - this.timeline_mouse = this.mouse; - } - - // When the mouse clicks the screen. - onMouseClick() { - if (this.camera == undefined) { return; } - const camera_pos = new THREE.Vector3( - parseFloat(this.camera.position.x.toFixed(2)), - parseFloat(this.camera.position.y.toFixed(2)), - parseFloat(this.camera.position.z.toFixed(2)), - ); - if (this.camera_last_pos.equals(new THREE.Vector3(0, 0, 0))) { - this.camera_last_pos.copy(camera_pos); - } - - if ( - this.raycaster == undefined || - this.mouse == undefined || - this.control == undefined || - this.outlinePass == undefined || - this.camera_person_mode || - !this.camera_last_pos.equals(camera_pos) - ) { - this.camera_last_pos.copy(camera_pos); - return; - } - this.camera_last_pos.copy(camera_pos); - - this.raycaster.setFromCamera(this.mouse, this.camera); - const interactable: any[] = []; - this.scene.children.forEach((child: THREE.Object3D) => { - if (child.name != "") { - if ( - child.type == "Mesh" || - child.type == "Object3D" || - child.type == "Group" || - child.type == "SkinnedMesh" - ) { - interactable.push(child); - } - } + async onkeydown(event: KeyboardEvent) { + if (hotkeysStatus.value.disabled) { + return; + } else if (event.key === "f" && this.selected && this.orbitControls) { + this.focus(); + return; + } else if (event.key === " ") { + if (!this.rendering && !this.switchPreviewToggle && this.selectedCanvas) { + this.togglePlayback(); + } + return; + } else if (event.key === "Backspace" || event.key === "Delete") { + if (this.selected) { + this.selected.forEach((selected) => { + this.deleteObject(selected.uuid); }); - const intersects = this.raycaster.intersectObjects(interactable, true); - - if (intersects.length > 0) { - if (intersects[0].object.type != "GridHelper") { - let currentObject = intersects[0].object; - while (currentObject.parent && currentObject.parent.type !== "Scene") { - currentObject = currentObject.parent; - } - - this.selected = []; - - // Show panel here - if (currentObject.type == "Scene") { - this.selected?.push(intersects[0].object); - } - else { - this.selected?.push(currentObject); - } - - this.selectObject(currentObject); - } - } else { - this.selected = []; - this.setSelected(this.selected); - this.removeTransformControls(); - hideObjectPanel(); - } - - if (this.sceneManager) { - const selected: SceneObject | null = this.sceneManager.selected(); - outlinerState.selectedItem.value = selected; - } + } + return; + } else if (event.key === "t") { + // transform + this.control?.setMode("translate"); + return; + } else if (event.key === "r") { + // rotate + this.control?.setMode("rotate"); + return; + } else if (event.key === "g") { + // scale + this.control?.setMode("scale"); + return; } + + if (event.ctrlKey && !this.isProcessing) { + if (event.key === "z") { + // undo + this.isProcessing = true; + await this.sceneManager?.undo(); + this.isProcessing = false; + return; + } else if (event.key === "y") { + // redo + this.isProcessing = true; + await this.sceneManager?.redo(); + this.isProcessing = false; + return; + } + } + } + + // Sets new mouse location usually used in raycasts. + onMouseMove(event: MouseEvent) { + if (this.canvReference == undefined) { + return; + } + const rect = this.canvReference.getBoundingClientRect(); + if (this.mouse == undefined) { + return; + } + this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + this.timeline_mouse = this.mouse; + } + + // When the mouse clicks the screen. + onMouseClick() { + if (this.camera == undefined) { + return; + } + const camera_pos = new THREE.Vector3( + parseFloat(this.camera.position.x.toFixed(2)), + parseFloat(this.camera.position.y.toFixed(2)), + parseFloat(this.camera.position.z.toFixed(2)), + ); + if (this.camera_last_pos.equals(new THREE.Vector3(0, 0, 0))) { + this.camera_last_pos.copy(camera_pos); + } + + if ( + this.raycaster == undefined || + this.mouse == undefined || + this.control == undefined || + this.outlinePass == undefined || + this.camera_person_mode || + !this.camera_last_pos.equals(camera_pos) + ) { + this.camera_last_pos.copy(camera_pos); + return; + } + this.camera_last_pos.copy(camera_pos); + + this.raycaster.setFromCamera(this.mouse, this.camera); + const interactable: any[] = []; + this.scene.children.forEach((child: THREE.Object3D) => { + if (child.name != "") { + if ( + child.type == "Mesh" || + child.type == "Object3D" || + child.type == "Group" || + child.type == "SkinnedMesh" + ) { + interactable.push(child); + } + } + }); + const intersects = this.raycaster.intersectObjects(interactable, true); + + if (intersects.length > 0) { + if (intersects[0].object.type != "GridHelper") { + let currentObject = intersects[0].object; + while (currentObject.parent && currentObject.parent.type !== "Scene") { + currentObject = currentObject.parent; + } + + this.selected = []; + + // Show panel here + if (currentObject.type == "Scene") { + this.selected?.push(intersects[0].object); + } else { + this.selected?.push(currentObject); + } + + this.selectObject(currentObject); + } + } else { + this.selected = []; + this.setSelected(this.selected); + this.removeTransformControls(); + hideObjectPanel(); + } + + if (this.sceneManager) { + const selected: SceneObject | null = this.sceneManager.selected(); + outlinerState.selectedItem.value = selected; + } + } } diff --git a/app/pages/PageEnigma/Editor/scene.ts b/app/pages/PageEnigma/Editor/scene.ts index 17d410f3b5..0ca5bf69ac 100644 --- a/app/pages/PageEnigma/Editor/scene.ts +++ b/app/pages/PageEnigma/Editor/scene.ts @@ -90,6 +90,32 @@ class Scene { return obj; } + get_objects_name(uuid: string): string { + const object = this.get_object_by_uuid(uuid); + if (object) { + return object.name; + } + return uuid; + } + + get_objects_position(uuid: string): THREE.Vector3 { + const object = this.get_object_by_uuid(uuid); + let pos = new THREE.Vector3(0, 0, 0); + if (object) { + pos = pos.copy(object.position); + } + return pos; + } + + get_objects_userdata(uuid: string): Record { + const object = this.get_object_by_uuid(uuid); + let userdata = {}; + if (object) { + userdata = object.userData; + } + return userdata; + } + get_object_by_uuid(uuid: string) { return this.scene.getObjectByProperty("uuid", uuid); } @@ -272,9 +298,10 @@ class Scene { } setVisible(object_uuid: string, visible: boolean) { - const object = this.get_object_by_uuid(object_uuid); + let object = this.get_object_by_uuid(object_uuid); if (object) { object.visible = visible; + object.userData["visible"] = object.visible; } } diff --git a/app/pages/PageEnigma/Editor/scene_manager_api.ts b/app/pages/PageEnigma/Editor/scene_manager_api.ts index 34aaac1b90..34b542bf7d 100644 --- a/app/pages/PageEnigma/Editor/scene_manager_api.ts +++ b/app/pages/PageEnigma/Editor/scene_manager_api.ts @@ -1,180 +1,443 @@ import * as THREE from "three"; -import { IconDefinition, faCamera, faCube, faPerson } from "@fortawesome/pro-solid-svg-icons"; +import { + IconDefinition, + faCamera, + faCube, + faPerson, +} from "@fortawesome/pro-solid-svg-icons"; import Scene from "./scene"; import { MouseControls } from "./keybinds_controls"; import { ClipGroup } from "~/enums"; +import { + CommandInputTypes, + CreationSceneItem, + DeletionData, + DeletionSceneItem, + ICommand, + SceneState, + TransformData, + TransformSceneItem, + UserData, + UserDataSceneItem, +} from "./Commands"; export type SceneObject = { - id: string; - icon: IconDefinition; - name: string; - type: string; - visible: boolean; - locked: boolean; + id: string; + icon: IconDefinition; + name: string; + type: string; + visible: boolean; + locked: boolean; }; export interface SceneManagerAPI { - create(media_token: string, name: string, position: THREE.Vector3): void; - retrieve(object_uuid: string): void; - delete(object_uuid: string): void; - update(): void; - selected(): void; - render_outliner(timeline_characters: { [key: string]: ClipGroup }): void; - onMouseMove(event: MouseEvent): void; - onMouseClick(): void; - onKeyDown(event: KeyboardEvent): void; - onMouseDown(event: MouseEvent): void; - onMouseUp(event: MouseEvent): void; - select_object(id: string): void; + create(media_token: string, name: string, position: THREE.Vector3): void; + retrieve(object_uuid: string): void; + delete(object_uuid: string): void; + update( + object_uuid: string, + position: THREE.Vector3, + rotation: THREE.Euler, + scale: THREE.Vector3, + ): void; + undo(): void; + redo(): void; + selected(): void; + render_outliner(timeline_characters: { [key: string]: ClipGroup }): void; + onMouseMove(event: MouseEvent): void; + onMouseClick(): void; + onKeyDown(event: KeyboardEvent): void; + onMouseDown(event: MouseEvent): void; + onMouseUp(event: MouseEvent): void; + select_object(id: string): void; } +type UpdateOutliner = () => void; + export class SceneManager implements SceneManagerAPI { - scene: Scene; - mouse_controls: MouseControls; - version: number; - selected_objects: THREE.Object3D[] | undefined; + scene: Scene; + mouse_controls: MouseControls; + version: number; + selected_objects: THREE.Object3D[] | undefined; + private updateOutliner: UpdateOutliner; + public undoStack: ICommand[] = []; + private undoIndex: number = 0; + private lastSceneState: SceneState; - constructor(version: number, mouse_controls: MouseControls, scene: Scene, devMode: boolean = false) { - this.mouse_controls = mouse_controls; - this.scene = scene; - this.version = version + constructor( + version: number, + mouse_controls: MouseControls, + scene: Scene, + devMode: boolean = false, + updateOutliner: UpdateOutliner, + ) { + this.mouse_controls = mouse_controls; + this.scene = scene; + this.version = version; + this.lastSceneState = this.getSceneState(); + this.updateOutliner = updateOutliner; - if (devMode) { - window.addEventListener( - "mousemove", - this.onMouseMove.bind(this), - false, - ); - window.addEventListener( - "click", - this.onMouseClick.bind(this), - false, - ); - window.addEventListener( - "keydown", - this.onKeyDown.bind(this), - false, - ); - window.addEventListener( - "mousedown", - this.onMouseDown.bind(this), - false, - ); - window.addEventListener( - "mouseup", - this.onMouseUp.bind(this), - false, - ); + if (devMode) { + window.addEventListener("mousemove", this.onMouseMove.bind(this), false); + window.addEventListener("click", this.onMouseClick.bind(this), false); + window.addEventListener("keydown", this.onKeyDown.bind(this), false); + window.addEventListener("mousedown", this.onMouseDown.bind(this), false); + window.addEventListener("mouseup", this.onMouseUp.bind(this), false); + } + } + + public async undo() { + this.undoIndex += 1; + if (this.undoIndex >= this.undoStack.length) { + this.undoIndex = this.undoStack.length; + } + const undoCommand = this.undoStack.at( + this.undoStack.length - this.undoIndex, + ); + await undoCommand?.undo(); + this.updateOutliner(); // In the future we will address this because of its relational issues with editor and the current class. + this.lastSceneState = this.getSceneState(); + } + + public async redo() { + const undoCommand = this.undoStack.at( + this.undoStack.length - this.undoIndex, + ); + await undoCommand?.redo(); + this.undoIndex -= 1; + if (this.undoIndex <= -1) { + this.undoIndex = 0; + } + this.updateOutliner(); // In the future we will address this because of its relational issues with editor and the current class. + this.lastSceneState = this.getSceneState(); + } + + public async create( + media_token: string, + name: string, + position: THREE.Vector3, + ): Promise> { + if (media_token !== "Parim") { + return await this.scene.loadObject( + media_token, + name, + true, + position, + this.version, + ); + } else { + return await this.scene.instantiate(name, position); + } + } + + /* NEVER CALL THIS INTERNALLY */ + public render_outliner(timeline_characters: { [key: string]: ClipGroup }) { + // needs timeline_characters to render favicons. + // Not permanent just in place until we have multi object select ability. + const selected_item = this.selected(); + const signal_items: SceneObject[] = []; + this.scene.scene.children.forEach((child) => { + const converted = this.convert_object(child, timeline_characters); + if (converted.name !== "") { + signal_items.push(converted); + } + }); + const outlinerState = { + selectedItem: selected_item, + items: signal_items, + }; + return outlinerState; + } + + public async retrieve(object_uuid: string) { + return this.scene.get_object_by_uuid(object_uuid); + } + + public async update( + object_uuid: string, + position: THREE.Vector3, + rotation: THREE.Euler, + scale: THREE.Vector3, + ) { + const object = await this.retrieve(object_uuid); + if (object) { + object.position.copy(position); + object.rotation.copy(rotation); + object.scale.copy(scale); + } + } + + public async delete(object_uuid: string) { + // Deletes an object. + this.mouse_controls.deleteObject(object_uuid); + } + + public async double_click() { + this.mouse_controls.focus(); + } + + public async hideObject(object_uuid: string) { + const object = await this.retrieve(object_uuid); + if (object?.visible !== undefined) { + object.visible = !object.visible; + object.userData["visible"] = object.visible; + } + } + + public selected() { + let selected_item = null; + if (this.selected_objects && this.selected_objects.length > 0) { + selected_item = this.selected_objects[0]; + } + if (selected_item) { + return this.convert_object(selected_item, {}); + } + return null; + } + + public select_object(id: string) { + const object = this.scene.get_object_by_uuid(id); + if (object) { + this.mouse_controls.selected = [object]; + this.mouse_controls.selectObject(object); + } + } + + // Converts a 3d object to signal item format. + private convert_object( + object: THREE.Object3D, + timeline_characters: { [key: string]: ClipGroup }, + ) { + let faicon = faCube; + let name = object.name; + if (object.name == "::CAM::") { + faicon = faCamera; + name = "Camera"; + } else if (object.uuid in timeline_characters) { + faicon = faPerson; + } + let locked = object.userData["locked"]; + if (locked == undefined) { + locked = false; + } + return { + id: object.uuid, + icon: faicon, + name: name.charAt(0).toUpperCase() + name.slice(1), + type: object.type, + visible: object.visible, + locked: object.userData["locked"], + }; + } + + public onMouseMove(event: MouseEvent) { + this.mouse_controls.onMouseMove(event); + } + + public onMouseClick() { + this.mouse_controls.onMouseClick(); + } + + public onKeyDown(event: KeyboardEvent) { + this.mouse_controls.onkeydown(event); + } + + public onMouseDown(event: MouseEvent) { + this.mouse_controls.onMouseDown(event); + } + + public onMouseUp(event: MouseEvent) { + this.mouse_controls.onMouseUp(event); + this.check_scene_for_updates(); + } + + private getSceneState(): SceneState { + const scene_objects: string[] = []; + const scene_names: string[] = []; + const scene_positions: string[] = []; + const scene_userdata: string[] = []; + const scene_rotations: string[] = []; + const scene_scales: string[] = []; + + this.scene.scene.children.forEach((child) => { + if (child.name != "") { + scene_objects.push(child.uuid); + scene_names.push(child.name); + scene_userdata.push(JSON.stringify(child.userData)); + scene_positions.push(JSON.stringify(child.position.clone())); + scene_rotations.push(JSON.stringify(child.rotation.clone())); + scene_scales.push(JSON.stringify(child.scale.clone())); + } + }); + return { + scene_objects: scene_objects, + scene_names: scene_names, + scene_userdata: scene_userdata, + scene_positions: scene_positions, + scene_rotations: scene_rotations, + scene_scales: scene_scales, + }; + } + + private is_creation(sceneState: SceneState): string { + let resp = ""; + sceneState.scene_objects.forEach((object) => { + if (!this.lastSceneState.scene_objects.includes(object)) { + resp = object; + return resp; + } + }); + return resp; + } + + private is_deletion(sceneState: SceneState): string { + let resp = ""; + this.lastSceneState.scene_objects.forEach((object) => { + if (sceneState.scene_objects.includes(object) == false) { + resp = object; + return resp; + } + }); + return resp; + } + + private is_userdata(sceneState: SceneState): string { + let resp = ""; + this.lastSceneState.scene_userdata.forEach((object) => { + if (sceneState.scene_userdata.includes(object) == false) { + const respIdx = this.lastSceneState.scene_userdata.indexOf(object); + resp = sceneState.scene_objects[respIdx]; + return sceneState.scene_objects[respIdx]; + } + }); + return resp; + } + + private is_transformation(sceneState: SceneState): string { + let resp = ""; + if (resp === "") { + sceneState.scene_positions.forEach((object) => { + if (this.lastSceneState.scene_positions.includes(object) == false) { + const respIdx = sceneState.scene_positions.indexOf(object); + resp = sceneState.scene_objects[respIdx]; + return sceneState.scene_objects[respIdx]; } + }); } - - public async create(media_token: string, name: string, position: THREE.Vector3) { - await this.scene.loadObject(media_token, name, true, position, this.version); - } - - /* NEVER CALL THIS INTERNALLY */ - public render_outliner(timeline_characters: { [key: string]: ClipGroup }) { // needs timeline_characters to render favicons. - console.log("Render Outliner.") - // Not permanent just in place until we have multi object select ability. - let selected_item = this.selected(); - let signal_items: SceneObject[] = []; - this.scene.scene.children.forEach(child => { - const converted = this.convert_object(child, timeline_characters) - if(converted.name !== ""){ - signal_items.push(converted) - } - }); - const outlinerState = { - selectedItem: selected_item, - items: signal_items + if (resp === "") { + sceneState.scene_rotations.forEach((object) => { + if (this.lastSceneState.scene_rotations.includes(object) == false) { + const respIdx = sceneState.scene_rotations.indexOf(object); + resp = sceneState.scene_objects[respIdx]; + return sceneState.scene_objects[respIdx]; } - return outlinerState; + }); } - - public async retrieve(object_uuid: string) { - return this.scene.get_object_by_uuid(object_uuid); - } - - public async update() { - // We dont use parents so we will see what this will do. - } - - public async delete(object_uuid: string) { - // Deletes an object. - this.mouse_controls.deleteObject(object_uuid); - } - - public async double_click(object_uuid: string) { - this.mouse_controls.focus(); - } - - public async hideObject(object_uuid: string) { - let object = await this.retrieve(object_uuid); - if(object?.visible !== undefined){ - object.visible = !object.visible; - object.userData["visible"] = object.visible; + if (resp === "") { + sceneState.scene_scales.forEach((object) => { + if (this.lastSceneState.scene_scales.includes(object) == false) { + const respIdx = sceneState.scene_scales.indexOf(object); + resp = sceneState.scene_objects[respIdx]; + return sceneState.scene_objects[respIdx]; } + }); } + return resp; + } - public selected() { - let selected_item = null; - if(this.selected_objects && this.selected_objects.length > 0){ - selected_item = this.selected_objects[0]; + public async add_creation_undostack(object: THREE.Object3D) { + this.undoStack.push( + new CreationSceneItem(this, { + object_uuid: object.uuid, + name: object.name, + position: object.position.clone(), + userdata: object.userData, + }), + ); + this.lastSceneState = this.getSceneState(); + } + + /* Can be called Internally or Externally for undo redo stack */ + // This function allows you to capture transform states and occurs on mouse up. + public async check_scene_for_updates() { + const sceneState = this.getSceneState(); + if (JSON.stringify(this.lastSceneState) !== JSON.stringify(sceneState)) { + // Add Scene state for undo redo here. + const is_deleted = this.is_deletion(sceneState); + const is_transform = this.is_transformation(sceneState); + const is_userdata = this.is_userdata(sceneState); + + this.undoStack = this.undoStack.slice(-16); + + if (is_deleted != "") { + const objectIndex = + this.lastSceneState.scene_objects.indexOf(is_deleted); + const name = this.lastSceneState.scene_names[objectIndex]; + const position = this.lastSceneState.scene_positions[objectIndex]; + const userdata = JSON.parse( + this.lastSceneState.scene_userdata[objectIndex], + ); + + const undoPush: DeletionData = { + object_uuid: is_deleted, + name: name, + position: JSON.parse(position), + userdata: userdata, + }; + this.undoStack.push(new DeletionSceneItem(this, undoPush)); + } else if (is_userdata != "") { + const objectIndex = sceneState.scene_objects.indexOf(is_userdata); + const startUserdata = JSON.parse( + this.lastSceneState.scene_userdata[objectIndex], + ); + const userdata = JSON.parse(sceneState.scene_userdata[objectIndex]); + + const endPush: UserData = { + object_uuid: is_userdata, + color: userdata["color"], + locked: userdata["locked"], + visible: userdata["visible"], + }; + + const startPush: UserData = { + object_uuid: is_userdata, + color: startUserdata["color"], + locked: startUserdata["locked"], + visible: startUserdata["visible"], + }; + + this.undoStack.push(new UserDataSceneItem(this, startPush, endPush)); + } else if (is_transform != "") { + const objectIndexStart = + this.lastSceneState.scene_objects.indexOf(is_transform); + if ( + this.lastSceneState.scene_positions[objectIndexStart] !== undefined + ) { + const objectIndexEnd = sceneState.scene_objects.indexOf(is_transform); + + const startPush: TransformData = { + object_uuid: is_transform, + position: JSON.parse( + this.lastSceneState.scene_positions[objectIndexStart], + ), + rotation: JSON.parse( + this.lastSceneState.scene_rotations[objectIndexStart], + ), + scale: JSON.parse( + this.lastSceneState.scene_scales[objectIndexStart], + ), + }; + const endPush: TransformData = { + object_uuid: is_transform, + position: JSON.parse(sceneState.scene_positions[objectIndexEnd]), + rotation: JSON.parse(sceneState.scene_rotations[objectIndexEnd]), + scale: JSON.parse(sceneState.scene_scales[objectIndexEnd]), + }; + this.undoStack.push(new TransformSceneItem(this, startPush, endPush)); } - if(selected_item) { - return this.convert_object(selected_item, {}); - } - return null; - } - - public select_object(id: string) { - let object = this.scene.get_object_by_uuid(id); - if(object){ - this.mouse_controls.selected = [object]; - this.mouse_controls.selectObject(object); - } - } - - // Converts a 3d object to signal item format. - private convert_object(object: THREE.Object3D, timeline_characters: { [key: string]: ClipGroup }) { - let faicon = faCube; - let name = object.name; - if (object.name == "::CAM::") { - faicon = faCamera; - name = "Camera"; - } - else if (object.uuid in timeline_characters) { - faicon = faPerson; - } - let locked = object.userData["locked"]; - if(locked == undefined) { locked = false } - return { - id: object.uuid, - icon: faicon, - name: name.charAt(0).toUpperCase() + name.slice(1), - type: object.type, - visible: object.visible, - locked: object.userData["locked"], - } - } - - public onMouseMove(event: MouseEvent) { - this.mouse_controls.onMouseMove(event); - } - - public onMouseClick() { - this.mouse_controls.onMouseClick(); - } - - public onKeyDown(event: KeyboardEvent) { - this.mouse_controls.onkeydown(event); - } - - public onMouseDown(event: MouseEvent) { - this.mouse_controls.onMouseDown(event); - } - - public onMouseUp(event: MouseEvent) { - this.mouse_controls.onMouseUp(event); + } } + this.undoIndex = 0; + this.lastSceneState = sceneState; + } } diff --git a/app/pages/PageEnigma/Editor/timeline.ts b/app/pages/PageEnigma/Editor/timeline.ts index 9ed4f587e8..300becae31 100644 --- a/app/pages/PageEnigma/Editor/timeline.ts +++ b/app/pages/PageEnigma/Editor/timeline.ts @@ -28,6 +28,7 @@ import { GenerationOptions } from "~/pages/PageEnigma/models/generationOptions"; import { Vector3 } from "three"; import { outlinerState } from "../signals"; +import { CreationSceneItem } from "./Commands"; export class TimeLine { editorEngine: Editor; @@ -155,7 +156,8 @@ export class TimeLine { // Create operations case toEngineActions.ADD_CHARACTER: { const newObject = await this.addCharacter(data.data as MediaItem); - this.queueNewObjectMessage(newObject, data.data as MediaItem); + if (newObject) + this.queueNewObjectMessage(newObject, data.data as MediaItem); const result = this.editorEngine.sceneManager?.render_outliner( this.characters, ); @@ -164,7 +166,8 @@ export class TimeLine { } case toEngineActions.ADD_OBJECT: { const newObject = await this.addObject(data.data as MediaItem); - this.queueNewObjectMessage(newObject, data.data as MediaItem); + if (newObject) + this.queueNewObjectMessage(newObject, data.data as MediaItem); const result = this.editorEngine.sceneManager?.render_outliner( this.characters, ); @@ -175,7 +178,7 @@ export class TimeLine { case toEngineActions.ADD_SHAPE: { const newShape = await this.addShape(data.data as MediaItem); this.queueNewObjectMessage(newShape, data.data as MediaItem); - const result = this.editorEngine.sceneManager?.render_outliner( + let result = this.editorEngine.sceneManager?.render_outliner( this.characters, ); if (result) outlinerState.items.value = result.items; @@ -219,44 +222,45 @@ export class TimeLine { const pos = this.getPos(); const new_data = { ...data }; - const obj = await this.scene.loadObject( + const obj = await this.editorEngine.sceneManager?.create( media_id, name, - true, pos, - this.editorEngine.version, ); - obj.userData["name"] = name; - obj.name = name; - obj.position.copy(pos); - const object_uuid = obj.uuid; + if (obj) { + obj.userData["name"] = name; + obj.name = name; + obj.position.copy(pos); + const object_uuid = obj.uuid; - this.characters[object_uuid] = ClipGroup.CHARACTER; // TODO: Create a class to make the idea of a character. - new_data["object_uuid"] = object_uuid; + this.characters[object_uuid] = ClipGroup.CHARACTER; // TODO: Create a class to make the idea of a character. + new_data["object_uuid"] = object_uuid; - Queue.publish({ - queueName: QueueNames.FROM_ENGINE, - action: fromEngineActions.UPDATE_CHARACTER_ID, - data: new_data, - }); + Queue.publish({ + queueName: QueueNames.FROM_ENGINE, + action: fromEngineActions.UPDATE_CHARACTER_ID, + data: new_data, + }); - this.addPlayableClip( - new ClipUI( - data.version, - ClipType.FAKE, - ClipGroup.CHARACTER, - "Default", - media_id, - obj.uuid, - obj.uuid, - name, - 0, - 0, - 0, - ), - ); + this.addPlayableClip( + new ClipUI( + data.version, + ClipType.FAKE, + ClipGroup.CHARACTER, + "Default", + media_id, + obj.uuid, + obj.uuid, + name, + 0, + 0, + 0, + ), + ); + await this.editorEngine.sceneManager?.add_creation_undostack(obj); + } return obj; } @@ -315,22 +319,34 @@ export class TimeLine { const pos = this.getPos(); const media_id = data.media_id; const name = data.name; - const obj = await this.scene.loadGlbWithPlaceholder( + // const obj = await this.scene.loadGlbWithPlaceholder( + // media_id, + // name, + // true, + // pos, + // this.editorEngine.version, + // ); + + const obj = await this.editorEngine.sceneManager?.create( media_id, name, - true, pos, - this.editorEngine.version, ); - obj.userData["name"] = name; - obj.name = name; - obj.position.copy(pos); + if (obj) { + obj.userData["name"] = name; + obj.name = name; + obj.position.copy(pos); + + await this.editorEngine.sceneManager?.add_creation_undostack(obj); + } return obj; } public async addShape(data: MediaItem) { const pos = this.getPos(); - return this.editorEngine.create_parim(data.media_id, pos); + const parim = this.editorEngine.create_parim(data.media_id, pos); + await this.editorEngine.sceneManager?.add_creation_undostack(parim); + return parim; } public async addKeyFrame(data: Keyframe) { diff --git a/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx b/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx index 6b2e5f4e58..1c9c1c71ba 100644 --- a/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx +++ b/app/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx @@ -236,8 +236,7 @@ export const ControlPanelSceneObject = () => {

- {currentSceneObject.object_name.charAt(0).toUpperCase() + - currentSceneObject.object_name.slice(1)} + {currentSceneObject.object_name.charAt(0).toUpperCase() + currentSceneObject.object_name.slice(1)}

{ } }); - const handleShowOutliner = () => { - outlinerIsShowing.value = !outlinerIsShowing.value; - }; + const handleShowOutliner = () => { + outlinerIsShowing.value = !outlinerIsShowing.value; + }; return (
@@ -324,6 +324,7 @@ export const ControlsTopButtons = () => { ]} /> + + { const isSelected = outlinerState.selectedItem.value?.id === item.id; - const editorEngine = useContext(EngineContext); + + const editorEngine = useContext(EngineContext) // Delete object logic here const handleDeleteKeyPress = (event: React.KeyboardEvent) => { @@ -45,7 +46,7 @@ const OutlinerItem = ({ item }: { item: SceneObject }) => { // Double click logic here const handleDoubleClick = () => { console.log("Item double clicked:", item.id); - editorEngine?.sceneManager?.double_click(item.id); + editorEngine?.sceneManager?.double_click(); }; return ( @@ -72,10 +73,7 @@ const OutlinerItem = ({ item }: { item: SceneObject }) => {