feat: add cloning (#80)

This commit is contained in:
Jacob
2022-11-21 04:54:17 +01:00
committed by GitHub
parent 27e7d267ee
commit 47d7a0fa5d
8 changed files with 206 additions and 34 deletions

View File

@@ -11,8 +11,6 @@
"core:test": "npm run test -w packages/core",
"2d:build": "npm run build -w packages/2d",
"2d:watch": "npm run watch -w packages/2d",
"legacy:build": "npm run build -w packages/legacy",
"legacy:watch": "npm run watch -w packages/legacy",
"ui:build": "npm run build -w packages/ui",
"ui:dev": "npm run dev -w packages/ui",
"template:serve": "npm run serve -w packages/template",

View File

@@ -1,8 +1,8 @@
import {
clone,
compound,
computed,
initial,
interpolation,
Property,
property,
wrapper,
@@ -424,6 +424,7 @@ export class Layout extends Node {
public declare readonly scale: Signal<Vector2, this>;
@wrapper(Vector2)
@clone(false)
@property()
public declare readonly absoluteScale: Signal<Vector2, this>;
@@ -454,6 +455,7 @@ export class Layout extends Node {
public declare readonly position: Signal<Vector2, this>;
@wrapper(Vector2)
@clone(false)
@property()
public declare readonly absolutePosition: Signal<Vector2, this>;
@@ -470,8 +472,9 @@ export class Layout extends Node {
}
}
@clone(false)
@property()
public declare readonly absoluteRotation: Signal<Vector2, this>;
public declare readonly absoluteRotation: Signal<number, this>;
protected getAbsoluteRotation() {
const matrix = this.localToWorld();

View File

@@ -1,9 +1,9 @@
import {
compound,
computed,
getPropertiesOf,
initial,
initialize,
interpolation,
property,
wrapper,
} from '../decorators';
@@ -15,7 +15,7 @@ import {
Signal,
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {ComponentChild, ComponentChildren} from './types';
import {ComponentChild, ComponentChildren, NodeConstructor} from './types';
import {Promisable} from '@motion-canvas/core/lib/threading';
import {View2D, use2DView} from '../scenes';
import {TimingFunction} from '@motion-canvas/core/lib/tweening';
@@ -209,6 +209,7 @@ export class Node implements Promisable<Node> {
public readonly children = createSignal<Node[]>([]);
public readonly parent = createSignal<Node | null>(null);
public readonly properties = getPropertiesOf(this);
public constructor({children, ...rest}: NodeProps) {
initialize(this, {defaults: rest});
@@ -346,6 +347,88 @@ export class Node implements Promisable<Node> {
// do nothing
}
/**
* Create a copy of this node.
*
* @param customProps - Properties to override.
*/
public clone(customProps: NodeProps = {}): this {
const props: NodeProps & Record<string, any> = {...customProps};
if (this.children().length > 0) {
props.children ??= this.children().map(child => child.clone());
}
for (const key in this.properties) {
const meta = this.properties[key];
if (!meta.clone || key in props) continue;
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
props[key] = signal();
}
return this.instantiate(props);
}
/**
* Create a raw copy of this node.
*
* @remarks
* A raw copy preserves any reactive properties from the source node.
*
* @param customProps - Properties to override.
*/
public rawClone(customProps: NodeProps = {}): this {
const props: NodeProps & Record<string, any> = {...customProps};
if (this.children().length > 0) {
props.children ??= this.children().map(child => child.rawClone());
}
for (const key in this.properties) {
const meta = this.properties[key];
if (!meta.clone || key in props) continue;
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
props[key] = signal.raw();
}
return this.instantiate(props);
}
/**
* Create a reactive copy of this node.
*
* @remarks
* A reactive copy has all its properties dynamically updated to match the
* source node.
*
* @param customProps - Properties to override.
*/
public reactiveClone(customProps: NodeProps = {}): this {
const props: NodeProps & Record<string, any> = {...customProps};
if (this.children().length > 0) {
props.children ??= this.children().map(child => child.reactiveClone());
}
for (const key in this.properties) {
const meta = this.properties[key];
if (!meta.clone || key in props) continue;
const signal = (<Record<string, Signal<any>>>(<unknown>this))[key];
props[key] = () => signal();
}
return this.instantiate(props);
}
/**
* Create an instance of this node's class.
*
* @param props - Properties to pass to the constructor.
*/
public instantiate(props: NodeProps = {}): this {
return new (<NodeConstructor<NodeProps, this>>this.constructor)(props);
}
/**
* Whether this node should be cached or not.
*/

View File

@@ -29,6 +29,6 @@ export interface FunctionComponent<T = any> {
(props: T): Node | null;
}
export interface NodeConstructor<T = any> {
new (props: T): Node;
export interface NodeConstructor<TProps = any, TNode = Node> {
new (props: TProps): TNode;
}

View File

@@ -1,6 +1,5 @@
import {SignalValue, isReactive} from '@motion-canvas/core/lib/utils';
import {capitalize, PropertyMetadata} from './property';
import {addInitializer} from './initializers';
import {capitalize, getPropertyMeta} from './property';
/**
* Create a compound property decorator.
@@ -45,13 +44,29 @@ export function compound(
mapping: string[] | Record<string, string>,
): PropertyDecorator {
return (target: any, key) => {
const metaKey = `meta${capitalize(key.toString())}`;
const meta: PropertyMetadata<any> = target[metaKey];
const meta = getPropertyMeta<any>(target, key);
if (!meta) {
console.error(`Missing property decorator for "${key.toString()}"`);
return;
}
const entries = Array.isArray(mapping)
? mapping.map(key => [key, key])
: Object.entries(mapping);
meta.compound = true;
meta.clone = false;
for (const [, property] of entries) {
const propertyMeta = getPropertyMeta<any>(target, property);
if (!propertyMeta) {
console.error(
`Missing property decorator for "${property.toString()}"`,
);
return;
}
propertyMeta.compoundParent = key.toString();
}
target.constructor.prototype[`get${capitalize(key.toString())}`] =
function () {
const object = Object.fromEntries(
@@ -72,11 +87,5 @@ export function compound(
}
}
};
addInitializer(target, (instance: any, context: any) => {
if (key in context.defaults) {
instance[key](context.defaults[key]);
}
});
};
}

View File

@@ -24,6 +24,9 @@ export interface PropertyMetadata<T> {
default?: T;
interpolationFunction?: InterpolationFunction<T>;
wrapper?: new (value: any) => T;
clone?: boolean;
compoundParent?: string;
compound?: boolean;
}
export interface Property<
@@ -33,7 +36,7 @@ export interface Property<
> extends SignalGetter<TGetterValue>,
SignalSetter<TSetterValue>,
SignalTween<TSetterValue>,
SignalUtils<TOwner> {}
SignalUtils<TSetterValue, TOwner> {}
export type PropertyOwner<TGetterValue, TSetterValue> = {
[key: `get${Capitalize<string>}`]: SignalGetter<TGetterValue> | undefined;
@@ -146,9 +149,9 @@ export function createProperty<
Object.defineProperty(handler, 'reset', {
value: signal
? () => signal?.reset()
? signal.reset
: initial !== undefined
? () => setter(initial)
? () => setter(wrap(initial))
: () => node,
});
@@ -156,8 +159,12 @@ export function createProperty<
value: () => setter(getter()),
});
Object.defineProperty(handler, 'raw', {
value: signal?.raw ?? getter,
});
if (initial !== undefined && !signal) {
setter(initial);
setter(wrap(initial));
}
return handler;
@@ -165,6 +172,13 @@ export function createProperty<
const PROPERTIES = Symbol.for('properties');
export function getPropertyMeta<T>(
object: any,
key: string | symbol,
): PropertyMetadata<T> | null {
return object[PROPERTIES]?.[key] ?? null;
}
export function getPropertiesOf(
value: any,
): Record<string, PropertyMetadata<any>> {
@@ -214,7 +228,7 @@ export function property<T>(): PropertyDecorator {
lookup = target[PROPERTIES];
}
const meta = (lookup[key] = lookup[key] ?? {});
const meta = (lookup[key] = lookup[key] ?? {clone: true});
addInitializer(target, (instance: any, context: any) => {
instance[key] = createProperty(
instance,
@@ -232,6 +246,7 @@ export function property<T>(): PropertyDecorator {
*
* @remarks
* This decorator specifies the initial value of a property.
*
* Must be specified before the {@link property} decorator.
*
* @example
@@ -247,7 +262,7 @@ export function property<T>(): PropertyDecorator {
*/
export function initial<T>(value: T): PropertyDecorator {
return (target: any, key) => {
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
const meta = getPropertyMeta<T>(target, key);
if (!meta) {
console.error(`Missing property decorator for "${key.toString()}"`);
return;
@@ -261,8 +276,8 @@ export function initial<T>(value: T): PropertyDecorator {
*
* @remarks
* This decorator specifies the interpolation function of a property.
* The interpolation function is used when tweening between different values of
* the property.
* The interpolation function is used when tweening between different values.
*
* Must be specified before the {@link property} decorator.
*
* @example
@@ -280,7 +295,7 @@ export function interpolation<T>(
value: InterpolationFunction<T>,
): PropertyDecorator {
return (target: any, key) => {
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
const meta = getPropertyMeta<T>(target, key);
if (!meta) {
console.error(`Missing property decorator for "${key.toString()}"`);
return;
@@ -294,9 +309,12 @@ export function interpolation<T>(
*
* @remarks
* This decorator specifies the wrapper of a property.
* Instead of returning the raw value of the property, an instance of the
* wrapper is returned. The actual value is passed as the first parameter to the
* constructor.
* Instead of returning the raw value, an instance of the wrapper is returned.
* The actual value is passed as the first parameter to the constructor.
*
* If the wrapper class has a method called `lerp` it will be set as the
* default interpolation function for the property.
*
* Must be specified before the {@link property} decorator.
*
* @example
@@ -314,7 +332,7 @@ export function wrapper<T>(
value: (new (value: any) => T) & {lerp?: InterpolationFunction<T>},
): PropertyDecorator {
return (target: any, key) => {
const meta: PropertyMetadata<T> = target[PROPERTIES]?.[key];
const meta = getPropertyMeta<T>(target, key);
if (!meta) {
console.error(`Missing property decorator for "${key.toString()}"`);
return;
@@ -325,3 +343,36 @@ export function wrapper<T>(
}
};
}
/**
* Create a cloning property decorator.
*
* @remarks
* This decorator specifies whether the property should be copied over when
* cloning the node.
*
* By default, any property is copied.
*
* Must be specified before the {@link property} decorator.
*
* @example
* ```ts
* class Example {
* \@wrapper(Vector2)
* \@property()
* public declare offset: Signal<Vector2, this>;
* }
* ```
*
* @param value - Whether the property should be cloned.
*/
export function clone<T>(value = true): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMeta<T>(target, key);
if (!meta) {
console.error(`Missing property decorator for "${key.toString()}"`);
return;
}
meta.clone = value;
};
}

View File

@@ -3,7 +3,6 @@ import {
computed,
initial,
initialize,
interpolation,
property,
wrapper,
} from '../decorators';

View File

@@ -29,7 +29,7 @@ export interface SignalTween<TValue> {
): ThreadGenerator;
}
export interface SignalUtils<TReturn> {
export interface SignalUtils<TValue, TReturn> {
/**
* Reset the signal to its initial value (if one has been set).
*
@@ -59,13 +59,38 @@ export interface SignalUtils<TReturn> {
* ```
*/
save(): TReturn;
/**
* Get the raw value of this signal.
*
* @remarks
* If the signal was provided with a factory function, the function itself
* will be returned, without invoking it.
*
* This method can be used to create copies of signals.
*
* @example
* ```ts
* const a = createSignal(2);
* const b = createSignal(() => a);
* // b() == 2
*
* const bClone = createSignal(b.raw());
* // bClone() == 2
*
* a(4);
* // b() == 4
* // bClone() == 4
* ```
*/
raw(): SignalValue<TValue>;
}
export interface Signal<TValue, TReturn = void>
extends SignalSetter<TValue, TReturn>,
SignalGetter<TValue>,
SignalTween<TValue>,
SignalUtils<TReturn> {}
SignalUtils<TValue, TReturn> {}
const collectionStack: DependencyContext[] = [];
@@ -178,6 +203,10 @@ export function createSignal<TValue, TReturn = void>(
value: () => set(get()),
});
Object.defineProperty(handler, 'raw', {
value: () => current,
});
if (initial !== undefined) {
set(initial);
}