Skip to content

Commit

Permalink
Saveable project + prepare to expose editor to addons
Browse files Browse the repository at this point in the history
  • Loading branch information
nahkd123 committed Oct 9, 2024
1 parent 1821237 commit 5f3ac39
Show file tree
Hide file tree
Showing 24 changed files with 743 additions and 322 deletions.
3 changes: 2 additions & 1 deletion nahara-motion-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"svelte-check": "^3.8.5",
"tslib": "^2.6.3",
"typescript": "^5.5.3",
"vite": "^5.4.6"
"vite": "^5.4.6",
"@types/wicg-file-system-access": "^2023.10.5"
}
}
170 changes: 132 additions & 38 deletions nahara-motion-ui/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,82 @@
<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";
Expand All @@ -7,58 +85,74 @@
import PropertiesPane from "./ui/properties/PropertiesPane.svelte";
import TimelinePane from "./ui/timeline/TimelinePane.svelte";
import ViewportPane from "./ui/viewport/ViewportPane.svelte";
import PopupHost from "./ui/popup/PopupHost.svelte";
import PopupHost, { openPopupAt } 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";
let states: Record<string, TabState> = {
"default-files": { type: "files", state: {} },
"default-project": { type: "project", state: {} },
"default-outliner": { type: "outliner", state: {} },
"default-timeline": { type: "timeline", state: {} },
"default-animationGraph": { type: "animationGraph", state: {} },
"default-properties": { type: "properties", state: {} },
"default-modifiers": { type: "modifiers", state: {} },
"default-viewport": { type: "viewport", state: {} }
};
let layout: PaneLayout = {
type: "split",
direction: SplitDirection.LeftToRight,
firstSize: 300,
first: {
type: "split",
direction: SplitDirection.BottomToTop,
firstSize: 500,
first: { type: "tab", tabs: ["default-files", "default-project"], selected: "default-files" },
second: { type: "tab", tabs: ["default-outliner"], selected: "default-outliner" }
},
second: {
let layoutManager = new LayoutManagerImpl({
layout: {
type: "split",
direction: SplitDirection.BottomToTop,
direction: SplitDirection.LeftToRight,
firstSize: 300,
first: { type: "tab", tabs: ["default-timeline", "default-animationGraph"], selected: "default-timeline" },
first: {
type: "split",
direction: SplitDirection.BottomToTop,
firstSize: 500,
first: { type: "tab", tabs: ["default-project", "default-files"], selected: "default-project" },
second: { type: "tab", tabs: ["default-outliner"], selected: "default-outliner" }
},
second: {
type: "split",
direction: SplitDirection.RightToLeft,
direction: SplitDirection.BottomToTop,
firstSize: 300,
first: { type: "tab", tabs: ["default-properties", "default-modifiers"], selected: "default-properties" },
second: { type: "tab", tabs: ["default-viewport"], selected: "default-viewport" }
first: { type: "tab", tabs: ["default-timeline", "default-animationGraph"], selected: "default-timeline" },
second: {
type: "split",
direction: SplitDirection.RightToLeft,
firstSize: 300,
first: { type: "tab", tabs: ["default-properties", "default-modifiers"], selected: "default-properties" },
second: { type: "tab", tabs: ["default-viewport"], selected: "default-viewport" }
}
}
},
states: {
"default-files": { type: "files", state: {} },
"default-project": { type: "project", state: {} },
"default-outliner": { type: "outliner", state: {} },
"default-timeline": { type: "timeline", state: {} },
"default-animationGraph": { type: "animationGraph", state: {} },
"default-properties": { type: "properties", state: {} },
"default-modifiers": { type: "modifiers", state: {} },
"default-viewport": { type: "viewport", state: {} }
}
};
}, []);
const currentLayout = layoutManager.currentStore;
let editor = new EditorImpl(layoutManager);
</script>

<div class="app">
<div class="content">
<TopBar />
<PaneHost {layout} {states} component={type => {
if (type == "properties") return PropertiesPane;
if (type == "timeline") return TimelinePane;
if (type == "outliner") return OutlinerPane;
if (type == "viewport") return ViewportPane;
if (type == "animationGraph") return AnimationGraphPane;
return EmptyPane;
}} on:layoutupdate={e => layout = e.detail} />
<TopBar {editor} />
<PaneHost
layout={$currentLayout.layout}
states={$currentLayout.states}
component={type => {
switch (type) {
case "properties": return PropertiesPane;
case "timeline": return TimelinePane;
case "outliner": return OutlinerPane;
case "viewport": return ViewportPane;
case "animationGraph": return AnimationGraphPane;
case "project": return ProjectPane;
default: return EmptyPane;
};
}}
extra={{ editor }}
on:layoutupdate={e => $currentLayout.layout = e.detail}
/>
<MediaBar />
</div>
<PopupHost />
Expand Down
33 changes: 5 additions & 28 deletions nahara-motion-ui/src/appglobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,25 @@ export interface Seekhead {
position: number;
}

// TODO: Very big TODO - Move everything to IEditor and EditorImpl
// IEditor expose states of the editor to addons
// EditorImpl is the implementation of IEditor that exposes the states to addons

export namespace app {
let logger = new motion.utils.Logger("app");
logger.info("Initializing Nahara's Motion UI...");

let currentProject: motion.IProject = new motion.Project({
name: "Empty project",
description: "Describe your top tier motion graphics here!",
workingTime: 0
});
let currentScene: motion.IScene | undefined = currentProject.newScene({
name: "Scene",
size: { x: 1920, y: 1080 }
});

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

// Component stores
export const currentProjectStore = writable(currentProject);
export const currentSceneStore = writable<motion.IScene | undefined>(currentScene);
export const currentSelectionStore = writable<ObjectsSelection | undefined>();
export const currentSeekheadStore = writable(currentSeekhead);
export const playbackStateStore = writable<"forward" | "backward" | "paused">(playbackState);

export function getCurrentProject() { return currentProject; }
export function openProject(project: motion.IProject) {
logger.info(`Opening project: ${project.metadata.name ?? "<Unnamed>"}`);
currentProject = project;
currentProjectStore.set(currentProject);
openScene([...project.scenes][0]);
}

export function getCurrentScene() { return currentScene; }
export function openScene(scene: motion.IScene | undefined) {
logger.info(`Opening scene: ${scene ? scene.metadata.name ?? "<Unnamed>" : "<Unload>"}`)
currentScene = scene;
currentSceneStore.set(scene);
deselectAll();
}

export function getCurrentSelection() { return currentSelection; }
export function deselectAll() {
currentSelection = undefined;
Expand Down
71 changes: 63 additions & 8 deletions nahara-motion-ui/src/ui/bar/TopBar.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
<script lang="ts">
import type { IObjectContainer, ISceneContainerObject } from "@nahara/motion";
import { app } from "../../appglobal";
import { SimpleProject, type IObjectContainer, type ISceneContainerObject } from "@nahara/motion";
import { openMenuAt } from "../menu/MenuHost.svelte";
import { openPopupAt } from "../popup/PopupHost.svelte";
import RenderPane from "../render/RenderPane.svelte";
import type { EditorImpl } from "../../App.svelte";
import type { DropdownEntry } from "../menu/FancyMenu";
const currentProject = app.currentProjectStore;
const currentScene = app.currentSceneStore;
export let editor: EditorImpl;
const currentProject = editor.projectStore;
const currentScene = editor.sceneStore;
const allLayouts = editor.layout.allLayoutsStore;
function openFileMenu(e: MouseEvent) {
openMenuAt(e.clientX, e.clientY, [
...($currentScene ? [
{
type: "simple",
name: "Save scene",
async click(event) {
if (!$currentProject || !$currentScene) return;
await $currentProject.saveScene($currentScene);
},
}
] as DropdownEntry[] : []),
{
type: "simple",
name: "Open/New project",
async click(e) {
const handle = await showDirectoryPicker({ id: "project", mode: "readwrite" });
const meta = await SimpleProject.probeProjectMeta(handle);
if (!meta) {
// TODO show popup warning of project creation
}
const project = await SimpleProject.tryOpen(handle);
editor.openProject(project);
}
},
{
type: "simple",
name: "Render current scene",
Expand All @@ -35,6 +63,17 @@
}
]);
}
let title: string;
$: {
if ($currentProject) {
title = $currentProject.metadata?.name ?? "<unnamed project>";
if ($currentScene) title += " / " + ($currentScene.metadata?.name ?? "<unnamed scene>");
title += " · Nahara's Motion";
} else {
title = "Nahara's Motion";
}
}
</script>

<div class="menu-bar">
Expand All @@ -46,12 +85,28 @@
<div class="menu">Help</div>
</div>
<div class="middle">
<div class="menu">{$currentProject.metadata.name ?? "<unnamed project>"}{$currentScene ? ` / ${$currentScene.metadata.name ?? "<unnamed scene>"}` : ''}</div>
<div class="menu">{title}</div>
</div>
<div class="right">
<div class="tab selected">General</div>
<div class="tab">Design</div>
<div class="tab">Animation</div>
{#each $allLayouts as layout}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tab"
role="button"
tabindex="0"
on:click={() => {
editor.layout.current = layout.layout;
allLayouts.update(a => a);
}}
>{layout.name}</div>
{/each}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tab"
role="button"
tabindex="0"
on:click={() => editor.layout.add(`Layout ${$allLayouts.length + 1}`, structuredClone(editor.layout.current))}
>+</div>
</div>
</div>

Expand Down
4 changes: 3 additions & 1 deletion nahara-motion-ui/src/ui/graph/AnimationGraphPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
import type { DropdownEntry } from "../menu/FancyMenu";
import { openMenuAt } from "../menu/MenuHost.svelte";
import { clipboard } from "../../clipboard";
import type { EditorImpl } from "../../App.svelte";
export let state: any;
export let editor: EditorImpl;
let labelWidth = 200;
let verticalZoom: number | "auto" = "auto";
let verticalScroll = 0; // 0 at vertical center of graph
let horizontalZoom = 100;
let horizontalScroll = 0;
const currentScene = app.currentSceneStore;
const currentScene = editor.sceneStore;
const currentSelection = app.currentSelectionStore;
const seekhead = app.currentSeekheadStore;
Expand Down
4 changes: 3 additions & 1 deletion nahara-motion-ui/src/ui/outliner/OutlinerPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import MenuHost, { openMenuAt } from "../menu/MenuHost.svelte";
import * as motion from "@nahara/motion";
import Outliner from "./Outliner.svelte";
import type { EditorImpl } from "../../App.svelte";
export let state: any;
const currentScene = app.currentSceneStore;
export let editor: EditorImpl;
const currentScene = editor.sceneStore;
const selection = app.currentSelectionStore;
function findContainer(object?: motion.SceneObjectInfo): motion.IObjectContainer {
Expand Down
1 change: 1 addition & 0 deletions nahara-motion-ui/src/ui/pane/EmptyPane.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<script lang="ts">
export let state: any;
export let editor: any;
</script>
Loading

0 comments on commit 5f3ac39

Please sign in to comment.