feat: navigate to scene and node source (#144)

This commit is contained in:
Jacob
2023-01-20 04:08:53 +01:00
committed by GitHub
parent 09ab7f96af
commit 86d495d01a
11 changed files with 86 additions and 41 deletions

View File

@@ -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();

View File

@@ -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) {

View File

@@ -11,5 +11,6 @@ export function makeScene2D(
return {
klass: Scene2D,
config: runner,
stack: new Error().stack,
};
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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} />

View File

@@ -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>
);

View File

@@ -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;
}
}
}
}

View File

@@ -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;`,
` }`,