mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 14:57:56 -05:00
feat: navigate to scene and node source (#144)
This commit is contained in:
@@ -232,9 +232,11 @@ export class Node implements Promisable<Node> {
|
||||
public readonly parent = createSignal<Node | null>(null);
|
||||
public readonly properties = getPropertiesOf(this);
|
||||
public readonly key: string;
|
||||
public readonly creationStack?: string;
|
||||
|
||||
public constructor({children, key, ...rest}: NodeProps) {
|
||||
this.key = useScene2D()?.registerNode(this, key) ?? key ?? '';
|
||||
this.creationStack = new Error().stack;
|
||||
initialize(this, {defaults: rest});
|
||||
for (const {signal} of this) {
|
||||
signal.reset();
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import {
|
||||
FullSceneDescription,
|
||||
GeneratorScene,
|
||||
Inspectable,
|
||||
InspectedAttributes,
|
||||
InspectedElement,
|
||||
Scene,
|
||||
SceneMetadata,
|
||||
SceneRenderEvent,
|
||||
ThreadGeneratorFactory,
|
||||
} from '@motion-canvas/core/lib/scenes';
|
||||
import {endScene, startScene} from '@motion-canvas/core/lib/utils';
|
||||
import {Vector2} from '@motion-canvas/core/lib/types';
|
||||
import {Node, View2D} from '../components';
|
||||
import {Meta} from '@motion-canvas/core';
|
||||
|
||||
export class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
||||
private readonly view: View2D;
|
||||
@@ -19,11 +18,9 @@ export class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
||||
private nodeCounters: Record<string, number> = {};
|
||||
|
||||
public constructor(
|
||||
name: string,
|
||||
meta: Meta<SceneMetadata>,
|
||||
runnerFactory: ThreadGeneratorFactory<View2D>,
|
||||
description: FullSceneDescription<ThreadGeneratorFactory<View2D>>,
|
||||
) {
|
||||
super(name, meta, runnerFactory);
|
||||
super(description);
|
||||
startScene(this);
|
||||
this.view = new View2D();
|
||||
endScene(this);
|
||||
@@ -72,6 +69,7 @@ export class Scene2D extends GeneratorScene<View2D> implements Inspectable {
|
||||
if (!node) return null;
|
||||
|
||||
const attributes: Record<string, any> = {
|
||||
stack: node.creationStack,
|
||||
key: node.key,
|
||||
};
|
||||
for (const {key, meta, signal} of node) {
|
||||
|
||||
@@ -11,5 +11,6 @@ export function makeScene2D(
|
||||
return {
|
||||
klass: Scene2D,
|
||||
config: runner,
|
||||
stack: new Error().stack,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,13 +218,14 @@ export class Project {
|
||||
|
||||
const instances: Scene[] = [];
|
||||
for (const description of scenes) {
|
||||
const scene = new description.klass(
|
||||
description.name,
|
||||
description.meta,
|
||||
description.config,
|
||||
);
|
||||
scene.project = this;
|
||||
description.onReplaced?.subscribe(config => scene.reload(config), false);
|
||||
const scene = new description.klass({
|
||||
...description,
|
||||
project: this,
|
||||
});
|
||||
description.onReplaced?.subscribe(description => {
|
||||
scene.creationStack = description.stack;
|
||||
scene.reload(description.config);
|
||||
}, false);
|
||||
scene.onReloaded.subscribe(() => this.reloaded.dispatch());
|
||||
instances.push(scene);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@ import {EventDispatcher, ValueDispatcher} from '../events';
|
||||
import {Project} from '../Project';
|
||||
import {decorate, threadable} from '../decorators';
|
||||
import {endScene, setProject, startScene} from '../utils';
|
||||
import {CachedSceneData, Scene, SceneMetadata, SceneRenderEvent} from './Scene';
|
||||
import {
|
||||
CachedSceneData,
|
||||
FullSceneDescription,
|
||||
Scene,
|
||||
SceneMetadata,
|
||||
SceneRenderEvent,
|
||||
} from './Scene';
|
||||
import {LifecycleEvents} from './LifecycleEvents';
|
||||
import {Threadable} from './Threadable';
|
||||
import {Rect, Vector2} from '../types';
|
||||
@@ -31,18 +37,12 @@ export interface ThreadGeneratorFactory<T> {
|
||||
export abstract class GeneratorScene<T>
|
||||
implements Scene<ThreadGeneratorFactory<T>>, Threadable
|
||||
{
|
||||
public readonly name: string;
|
||||
public readonly project: Project;
|
||||
public readonly meta: Meta<SceneMetadata>;
|
||||
public readonly timeEvents: TimeEvents;
|
||||
public random: Random;
|
||||
|
||||
public get project(): Project {
|
||||
if (!this.currentProject) {
|
||||
throw new Error(`Project for Scene ${this.name} has not been set.`);
|
||||
}
|
||||
return this.currentProject;
|
||||
}
|
||||
public set project(value: Project) {
|
||||
this.currentProject = value;
|
||||
}
|
||||
public creationStack?: string;
|
||||
|
||||
public get firstFrame() {
|
||||
return this.cache.current.firstFrame;
|
||||
@@ -95,19 +95,23 @@ export abstract class GeneratorScene<T>
|
||||
return this.previousScene;
|
||||
}
|
||||
|
||||
private runnerFactory: ThreadGeneratorFactory<T>;
|
||||
private previousScene: Scene | null = null;
|
||||
private currentProject: Project | null = null;
|
||||
private runner: ThreadGenerator | null = null;
|
||||
private state: SceneState = SceneState.Initial;
|
||||
private cached = false;
|
||||
private counters: Record<string, number> = {};
|
||||
|
||||
public constructor(
|
||||
public readonly name: string,
|
||||
public readonly meta: Meta<SceneMetadata>,
|
||||
private runnerFactory: ThreadGeneratorFactory<T>,
|
||||
description: FullSceneDescription<ThreadGeneratorFactory<T>>,
|
||||
) {
|
||||
decorate(this.runnerFactory, threadable(name));
|
||||
this.name = description.name;
|
||||
this.project = description.project;
|
||||
this.meta = description.meta;
|
||||
this.runnerFactory = description.config;
|
||||
this.creationStack = description.stack;
|
||||
|
||||
decorate(this.runnerFactory, threadable(this.name));
|
||||
this.timeEvents = new TimeEvents(this);
|
||||
|
||||
let seed = this.meta.getData().seed;
|
||||
|
||||
@@ -11,7 +11,10 @@ export type InspectedElement = unknown;
|
||||
/**
|
||||
* Represents attributes of an inspected element.
|
||||
*/
|
||||
export type InspectedAttributes = Record<string, any>;
|
||||
export type InspectedAttributes = {
|
||||
stack?: string;
|
||||
[K: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scenes can implement this interface to make their components
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface SceneMetadata extends Metadata {
|
||||
* {@link SceneDescription.config}.
|
||||
*/
|
||||
export interface SceneConstructor<T> {
|
||||
new (name: string, meta: Meta<SceneMetadata>, config: T): Scene;
|
||||
new (description: FullSceneDescription<T>): Scene;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +44,10 @@ export interface SceneDescription<T = unknown> {
|
||||
* Configuration object.
|
||||
*/
|
||||
config: T;
|
||||
/**
|
||||
* The stack trace at the moment of creation.
|
||||
*/
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +58,8 @@ export interface SceneDescription<T = unknown> {
|
||||
export interface FullSceneDescription<T = unknown> extends SceneDescription<T> {
|
||||
name: string;
|
||||
meta: Meta<SceneMetadata>;
|
||||
onReplaced: ValueDispatcher<T>;
|
||||
project: Project;
|
||||
onReplaced: ValueDispatcher<FullSceneDescription<T>>;
|
||||
}
|
||||
|
||||
export type DescriptionOf<TScene> = TScene extends Scene<infer TConfig>
|
||||
@@ -115,10 +120,11 @@ export interface Scene<T = unknown> {
|
||||
/**
|
||||
* Reference to the project.
|
||||
*/
|
||||
project: Project;
|
||||
readonly project: Project;
|
||||
readonly timeEvents: TimeEvents;
|
||||
readonly random: Random;
|
||||
readonly meta: Meta<SceneMetadata>;
|
||||
creationStack?: string;
|
||||
|
||||
/**
|
||||
* The frame at which this scene starts.
|
||||
|
||||
@@ -5,6 +5,8 @@ import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||
import {Pane} from '../tabs';
|
||||
import {useInspection} from '../../contexts';
|
||||
import {AutoField} from '../fields';
|
||||
import {Button, Group, Label} from '../controls';
|
||||
import {findAndOpenFirstUserFile} from '../../utils';
|
||||
|
||||
export function Properties() {
|
||||
const {inspectedElement} = useInspection();
|
||||
@@ -16,13 +18,25 @@ export function Properties() {
|
||||
[scene],
|
||||
);
|
||||
|
||||
const attributes = useMemo(
|
||||
() => inspectable?.inspectAttributes(inspectedElement),
|
||||
[inspectedElement, inspectable, frame],
|
||||
);
|
||||
const [stack, attributes] = useMemo(() => {
|
||||
const attributes = inspectable?.inspectAttributes(inspectedElement);
|
||||
if (attributes) {
|
||||
const {stack, ...rest} = attributes;
|
||||
return [stack, rest];
|
||||
}
|
||||
return [undefined, undefined];
|
||||
}, [inspectedElement, inspectable, frame]);
|
||||
|
||||
return (
|
||||
<Pane title="Properties" id="properties-pane">
|
||||
{stack && (
|
||||
<Group>
|
||||
<Label />
|
||||
<Button onClick={() => findAndOpenFirstUserFile(stack)} main>
|
||||
GO TO SOURCE
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
{attributes
|
||||
? Object.entries(attributes).map(([key, value]) => (
|
||||
<AutoField label={key} value={value} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {Scene} from '@motion-canvas/core/lib/scenes';
|
||||
import {useScenes, useSubscribableValue} from '../../hooks';
|
||||
import {usePlayer, useTimelineContext} from '../../contexts';
|
||||
import {useMemo} from 'preact/hooks';
|
||||
import {findAndOpenFirstUserFile} from '../../utils';
|
||||
|
||||
export function SceneTrack() {
|
||||
const scenes = useScenes();
|
||||
@@ -63,9 +64,19 @@ function SceneClip({scene}: SceneClipProps) {
|
||||
className={styles.transition}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.sceneName} style={nameStyle}>
|
||||
<button
|
||||
className={styles.sceneName}
|
||||
style={nameStyle}
|
||||
title="Go to source"
|
||||
onMouseUp={async event => {
|
||||
event.stopPropagation();
|
||||
if (scene.creationStack) {
|
||||
await findAndOpenFirstUserFile(scene.creationStack);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{scene.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -110,12 +110,17 @@
|
||||
}
|
||||
|
||||
.sceneName {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
line-height: 24px;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 8px 12px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export default ({
|
||||
`if (import.meta.hot) {`,
|
||||
` import.meta.hot.accept();`,
|
||||
` if (import.meta.hot.data.onReplaced) {`,
|
||||
` description.onReplaced.current = description.config;`,
|
||||
` description.onReplaced.current = description;`,
|
||||
` } else {`,
|
||||
` import.meta.hot.data.onReplaced = description.onReplaced;`,
|
||||
` }`,
|
||||
|
||||
Reference in New Issue
Block a user