Skip to content

Commit

Permalink
Expose selections and playback to addons
Browse files Browse the repository at this point in the history
  • Loading branch information
nahkd123 committed Oct 10, 2024
1 parent a5d05e1 commit caca840
Show file tree
Hide file tree
Showing 23 changed files with 470 additions and 276 deletions.
84 changes: 3 additions & 81 deletions nahara-motion-ui/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,95 +1,17 @@
<script lang="ts" context="module">
export class EditorImpl implements motion.IEditor {
openedProject?: motion.IProject | undefined;
openedScene?: motion.IScene | undefined;
readonly projectStore: Writable<motion.IProject | undefined> = writable();
readonly sceneStore: Writable<motion.IScene | undefined> = writable();
constructor(
public readonly layout: LayoutManagerImpl
) {}
openProject(project: motion.IProject): void {
if (project == this.openedProject) return;
if (this.openedProject) this.closeProject();
this.openedProject = project;
this.projectStore.set(project);
}
closeProject(): void {
if (this.openedScene) this.closeScene();
this.openedProject = undefined;
this.projectStore.set(undefined);
}
openScene(scene: motion.IScene): void {
if (scene == this.openedScene) return;
if (this.openedScene) this.closeScene();
this.openedScene = scene;
this.sceneStore.set(scene);
}
closeScene(): void {
this.openedScene = undefined;
this.sceneStore.set(undefined);
}
}
interface EditorLayout extends motion.IEditorLayout {
states: Record<string, TabState>;
layout: PaneLayout;
}
export class LayoutManagerImpl implements motion.IEditorLayoutManager {
readonly currentStore: Writable<EditorLayout> = writable();
readonly allLayoutsStore: Writable<motion.EditorLayoutEntry[]> = writable([]);
constructor(
private _current: EditorLayout,
public readonly allLayouts: motion.EditorLayoutEntry[]
) {
this.currentStore.set(_current);
this.allLayoutsStore.set(allLayouts);
}
get current() { return this._current; }
set current(layout: motion.IEditorLayout) {
this._current = layout as EditorLayout;
this.currentStore.set(layout as EditorLayout);
}
add(name: string, layout: motion.IEditorLayout): motion.EditorLayoutEntry {
const entry: motion.EditorLayoutEntry = { name, layout: structuredClone(layout) };
this.allLayouts.push(entry);
this.allLayoutsStore.set(this.allLayouts);
return entry;
}
remove(entry: motion.EditorLayoutEntry): void {
const idx = this.allLayouts.indexOf(entry);
if (idx == -1) return;
this.allLayouts.splice(idx, 1);
this.allLayoutsStore.set(this.allLayouts);
}
}
</script>

<script lang="ts">
import * as motion from "@nahara/motion";
import MediaBar from "./ui/bar/MediaBar.svelte";
import TopBar from "./ui/bar/TopBar.svelte";
import MenuHost from "./ui/menu/MenuHost.svelte";
import OutlinerPane from "./ui/outliner/OutlinerPane.svelte";
import PaneHost, { SplitDirection, type PaneLayout, type TabState } from "./ui/pane/PaneHost.svelte";
import PaneHost, { SplitDirection } from "./ui/pane/PaneHost.svelte";
import PropertiesPane from "./ui/properties/PropertiesPane.svelte";
import TimelinePane from "./ui/timeline/TimelinePane.svelte";
import ViewportPane from "./ui/viewport/ViewportPane.svelte";
import PopupHost, { openPopupAt } from "./ui/popup/PopupHost.svelte";
import PopupHost from "./ui/popup/PopupHost.svelte";
import AnimationGraphPane from "./ui/graph/AnimationGraphPane.svelte";
import EmptyPane from "./ui/pane/EmptyPane.svelte";
import { writable, type Readable, type Writable } from "svelte/store";
import ProjectPane from "./ui/project/ProjectPane.svelte";
import { EditorImpl, LayoutManagerImpl } from "./App";
let layoutManager = new LayoutManagerImpl({
layout: {
Expand Down
189 changes: 189 additions & 0 deletions nahara-motion-ui/src/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as motion from "@nahara/motion";
import { writable, type Writable } from "svelte/store";
import type { PaneLayout, TabState } from "./ui/pane/PaneHost.svelte";

export class EditorImpl implements motion.IEditor {
openedProject?: motion.IProject | undefined;
openedScene?: motion.IScene | undefined;

readonly projectStore: Writable<motion.IProject | undefined> = writable();
readonly sceneStore: Writable<motion.IScene | undefined> = writable();

constructor(
public readonly layout: LayoutManagerImpl
) {}
selections = new SelectionsManagerImpl();
playback = new PlaybackManagerImpl();

openProject(project: motion.IProject): void {
if (project == this.openedProject) return;
if (this.openedProject) this.closeProject();
this.openedProject = project;
this.projectStore.set(project);
}

closeProject(): void {
if (this.openedScene) this.closeScene();
this.openedProject = undefined;
this.projectStore.set(undefined);
}

openScene(scene: motion.IScene): void {
if (scene == this.openedScene) return;
if (this.openedScene) this.closeScene();
this.openedScene = scene;
this.sceneStore.set(scene);
}

closeScene(): void {
this.selections.clear();
this.playback.changeState("paused");
this.playback.seekTo(0);
this.openedScene = undefined;
this.sceneStore.set(undefined);
}
}

interface EditorLayout extends motion.IEditorLayout {
states: Record<string, TabState>;
layout: PaneLayout;
}

export class LayoutManagerImpl implements motion.IEditorLayoutManager {
readonly currentStore: Writable<EditorLayout> = writable();
readonly allLayoutsStore: Writable<motion.EditorLayoutEntry[]> = writable([]);

constructor(
private _current: EditorLayout,
public readonly allLayouts: motion.EditorLayoutEntry[]
) {
this.currentStore.set(_current);
this.allLayoutsStore.set(allLayouts);
}

get current() { return this._current; }
set current(layout: motion.IEditorLayout) {
this._current = layout as EditorLayout;
this.currentStore.set(layout as EditorLayout);
}

add(name: string, layout: motion.IEditorLayout): motion.EditorLayoutEntry {
const entry: motion.EditorLayoutEntry = { name, layout: structuredClone(layout) };
this.allLayouts.push(entry);
this.allLayoutsStore.set(this.allLayouts);
return entry;
}

remove(entry: motion.EditorLayoutEntry): void {
const idx = this.allLayouts.indexOf(entry);
if (idx == -1) return;
this.allLayouts.splice(idx, 1);
this.allLayoutsStore.set(this.allLayouts);
}
}

export class SelectionsManagerImpl implements motion.ISelectionsManager {
readonly objects = new ObjectSelectionImpl<motion.SceneObjectInfo>();
readonly keyframes = new ObjectSelectionImpl<motion.Keyframe<any>>();
readonly timeline = new TimelineSelectionImpl();

clear() {
this.objects.clear();
this.keyframes.clear();
this.timeline.select(0, 0);
}
}

export class ObjectSelectionImpl<T> implements motion.IObjectSelection<T> {
multiple: T[] = [];
primary?: T | undefined = undefined;

readonly selectionStore: Writable<ObjectSelectionImpl<T>> = writable();
readonly multipleStore: Writable<T[]> = writable();
readonly primaryStore: Writable<T | undefined> = writable();

constructor() {
this.selectionStore.set(this);
this.multipleStore.set(this.multiple);
this.primaryStore.set(this.primary);
}

addToSelection(target: T): void {
if (!this.multiple.includes(target)) this.multiple.push(target);
this.primary = target;
this.sendUpdate();
}

removeFromSelection(target: T): void {
const idx = this.multiple.indexOf(target);
if (idx != -1) this.multiple.splice(idx, 1);
if (this.primary == target) this.primary = this.multiple.length > 0
? this.multiple[this.multiple.length - 1]
: undefined;
this.sendUpdate();
}

clear(): void {
this.multiple = [];
this.primary = undefined;
this.sendUpdate();
}

sendUpdate() {
this.multipleStore.set(this.multiple);
this.primaryStore.set(this.primary);
this.selectionStore.update(a => a);
}
}

export class TimelineSelectionImpl implements motion.ITimelineSelection {
startTime: number = 0;
endTime: number = 0;

readonly startTimeStore: Writable<number> = writable(0);
readonly endTimeStore: Writable<number> = writable(0);

select(from: number, to: number): void {
this.startTime = from; this.startTimeStore.set(from);
this.endTime = to; this.endTimeStore.set(to);
}
}

export class PlaybackManagerImpl implements motion.IPlaybackManager {
currentTime: number = 0;
deltaTime: number = 0;
state: motion.PlaybackState = "paused";
rate: number = 1;
fps: motion.PlaybackFPS = "vsync";

readonly currentTimeStore: Writable<number> = writable(0);
readonly stateStore: Writable<motion.PlaybackState> = writable("paused");

seekTo(time: number): void {
this.currentTime = time;
this.currentTimeStore.set(time);
}

changeState(state: motion.PlaybackState): void {
if (this.state == state) return;

if (state != "paused" && this.state == "paused") {
const self = this;
let lastTimestamp = -1;

function renderLoop(timestamp: number) {
let deltaTime = lastTimestamp == -1 ? 0 : (timestamp - lastTimestamp);
deltaTime *= self.state == "playing-backward" ? -self.rate : self.rate;
self.deltaTime = deltaTime;
self.seekTo(self.currentTime + deltaTime * self.rate); // TODO Match fps here
lastTimestamp = timestamp;
if (self.state != "paused") window.requestAnimationFrame(renderLoop);
}

window.requestAnimationFrame(renderLoop);
}

this.state = state;
this.stateStore.set(state);
}
}
80 changes: 0 additions & 80 deletions nahara-motion-ui/src/appglobal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as motion from "@nahara/motion";
import { writable } from "svelte/store";
import type { DropdownEntry, TreeDropdownEntry } from "./ui/menu/FancyMenu";

export interface ObjectsSelection {
Expand Down Expand Up @@ -29,85 +28,6 @@ export namespace app {
let logger = new motion.utils.Logger("app");
logger.info("Initializing Nahara's Motion UI...");

let currentSelection: ObjectsSelection | undefined = undefined;
let currentSeekhead: Seekhead = {
position: 0
};
let playbackState: "forward" | "backward" | "paused" = "paused";

// Component stores
export const currentSelectionStore = writable<ObjectsSelection | undefined>();
export const currentSeekheadStore = writable(currentSeekhead);
export const playbackStateStore = writable<"forward" | "backward" | "paused">(playbackState);

export function getCurrentSelection() { return currentSelection; }
export function deselectAll() {
currentSelection = undefined;
currentSelectionStore.set(undefined);
}
export function selectSingle(object: motion.SceneObjectInfo) {
currentSelection = { primary: object, multiple: [object] };
currentSelectionStore.set(currentSelection);
}
export function selectMulti(object: motion.SceneObjectInfo, allowDeselecting = true) {
if (!currentSelection) {
selectSingle(object);
return;
}

if (currentSelection.multiple.includes(object)) {
if (!allowDeselecting) {
currentSelection.primary = object;
currentSelectionStore.set(currentSelection);
return;
}

const idx = currentSelection.multiple.indexOf(object);
const [deselected] = currentSelection.multiple.splice(idx, 1);
if (deselected == currentSelection.primary) currentSelection.primary = currentSelection.multiple[idx];

if (!currentSelection.primary) {
if (currentSelection.multiple.length == 0) {
deselectAll();
return;
}

currentSelection.primary = currentSelection.multiple[0];
}

currentSelectionStore.set(currentSelection);
return;
}

currentSelection.multiple.push(object);
currentSelection.primary = object;
currentSelectionStore.set(currentSelection);
}

export function getSeekhead() { return structuredClone(currentSeekhead); }
export function updateSeekhead(seekhead: Seekhead) {
currentSeekhead = seekhead;
currentSeekheadStore.set(seekhead);
}
export function getPlaybackState() { return playbackState; }
export function setPlaybackState(state: typeof playbackState) {
playbackState = state;
playbackStateStore.set(state);

if (state != "paused") {
let prevTimestamp = -1;

function renderCallback(timestamp: number) {
const delta = timestamp - prevTimestamp;
if (prevTimestamp != -1) updateSeekhead({ position: currentSeekhead.position + delta });
if (playbackState != "paused") requestAnimationFrame(renderCallback);
prevTimestamp = timestamp;
}

requestAnimationFrame(renderCallback);
}
}

/**
* Create menu entries for adding object
*/
Expand Down
Loading

0 comments on commit caca840

Please sign in to comment.