@ringozz/react-godot 1.0.0-0 → 1.0.0-10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +308 -94
- package/package.json +8 -6
- package/src/index.ts +58 -58
- package/src/react-fiber.ts +315 -307
- package/src/react-hooks.ts +148 -22
- package/src/react-jsx.ts +45 -41
- package/src/react-types.ts +31 -15
package/src/react-hooks.ts
CHANGED
|
@@ -1,22 +1,148 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import type { Signal } from '@ringozz/godot';
|
|
6
|
-
import type
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import type { Signal } from '@ringozz/godot';
|
|
6
|
+
import type { Node } from '@ringozz/godot/Node';
|
|
7
|
+
import type { Object as Instance } from '@ringozz/godot/Object';
|
|
8
|
+
import { PropertyTweener } from '@ringozz/godot/PropertyTweener';
|
|
9
|
+
import type { GodotVar } from '@ringozz/godot/runtime';
|
|
10
|
+
import { Tween, type EaseType, type TransitionType } from '@ringozz/godot/Tween';
|
|
11
|
+
import type React from 'react';
|
|
12
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
|
|
14
|
+
// Tween/PropertyTweener are only type-used here; value-reference them so their
|
|
15
|
+
// `_R` class registration isn't tree-shaken (otherwise createTween's wrapper
|
|
16
|
+
// falls back to an ancestor class and loses pause/play/tweenProperty).
|
|
17
|
+
void Tween;
|
|
18
|
+
void PropertyTweener;
|
|
19
|
+
|
|
20
|
+
export function useMutableCallback<T>(fn: T): React.RefObject<T> {
|
|
21
|
+
const ref = useRef<T>(fn);
|
|
22
|
+
useEffect(() => void (ref.current = fn), [fn]);
|
|
23
|
+
return ref;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useSignal<T extends (...args: any[]) => any>(signal: Signal<T>, handler: T): void {
|
|
27
|
+
const ref = useMutableCallback(handler);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const fn = ((...args: any[]) => ref.current(...args)) as T;
|
|
30
|
+
signal.connect(fn);
|
|
31
|
+
return () => signal.disconnect(fn);
|
|
32
|
+
// do not specify 'signal' dependency, because it changes often.
|
|
33
|
+
// specify invariant contents instead.
|
|
34
|
+
/* oxlint-disable-next-line react-hooks/exhaustive-deps */
|
|
35
|
+
}, [signal.getObjectId(), signal.getName()]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- useTween: tween-as-prop-value ----
|
|
39
|
+
//
|
|
40
|
+
// useTween(create, deps) returns [spring, tweenRef]. `spring` holds one
|
|
41
|
+
// value-assigner per `to` key — a plain function spread onto the node
|
|
42
|
+
// (`<Node3D {...spring} />`). Instance.assign (C++) treats any function on a
|
|
43
|
+
// non-signal prop as a value-assigner and invokes it with (node, nativeName);
|
|
44
|
+
// each contributes its own prop's tweener to one shared native Tween created on
|
|
45
|
+
// the first invocation. `tweenRef.current` is that Tween (null until mounted).
|
|
46
|
+
// Re-rendering with the same deps keeps the same assigners (the reconciler's
|
|
47
|
+
// identity diff drops them, so nothing restarts); changing deps creates a new
|
|
48
|
+
// tween (the previous one is killed).
|
|
49
|
+
|
|
50
|
+
export type TweenValue = number | number[] | GodotVar;
|
|
51
|
+
|
|
52
|
+
export interface TweenConfig {
|
|
53
|
+
/** Seconds (default 1). */
|
|
54
|
+
duration?: number;
|
|
55
|
+
/** Tween.setTrans — the default for the tween's tweeners. */
|
|
56
|
+
transition?: TransitionType;
|
|
57
|
+
/** Tween.setEase — the default for the tween's tweeners. */
|
|
58
|
+
ease?: EaseType;
|
|
59
|
+
/** PropertyTweener.setCustomInterpolator — a [0,1]→[0,1] easing function. */
|
|
60
|
+
easing?: (t: number) => number;
|
|
61
|
+
/** Tween.setSpeedScale. */
|
|
62
|
+
speedScale?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TweenProps<T extends Record<string, TweenValue> = Record<string, TweenValue>> {
|
|
66
|
+
/** Start values, applied on the first build (mount) before animating to `to`. */
|
|
67
|
+
from?: Partial<T>;
|
|
68
|
+
/** Target values — one assigner is returned per key. */
|
|
69
|
+
to: T;
|
|
70
|
+
config?: TweenConfig;
|
|
71
|
+
/** Seconds; per-tweener PropertyTweener.setDelay. */
|
|
72
|
+
delay?: number;
|
|
73
|
+
/** Tween.setLoops (0 = infinite). */
|
|
74
|
+
loops?: number;
|
|
75
|
+
/** Snap to `to` instead of animating. */
|
|
76
|
+
immediate?: boolean;
|
|
77
|
+
onStart?: () => void;
|
|
78
|
+
/** Fires on natural completion only (Godot's `finished`); kills/unmount don't fire it. */
|
|
79
|
+
onFinished?: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type Spring<T extends Record<string, TweenValue>> = { [K in keyof T]: T[K] };
|
|
83
|
+
|
|
84
|
+
// tweenProperty/set reject JS arrays, so array goals are reconstructed as the
|
|
85
|
+
// property's value type (read via get(native).constructor).
|
|
86
|
+
function toValueType(node: Instance, native: string, value: TweenValue): unknown {
|
|
87
|
+
if (!Array.isArray(value)) return value;
|
|
88
|
+
const Ctor = (node.get(native) as any)?.constructor;
|
|
89
|
+
if (typeof Ctor === 'function') return new Ctor(...value);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeSpring<T extends Record<string, TweenValue>>(
|
|
94
|
+
tweenRef: { current: Tween | null },
|
|
95
|
+
props: TweenProps<T>,
|
|
96
|
+
): Spring<T> {
|
|
97
|
+
const config = props.config ?? {};
|
|
98
|
+
const spring = {} as Spring<T>;
|
|
99
|
+
let started = false;
|
|
100
|
+
let applyFrom = false;
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(props.to)) {
|
|
103
|
+
const fromValue = props.from?.[key];
|
|
104
|
+
const assigner = (node: Instance, native: string) => {
|
|
105
|
+
if (props.immediate) {
|
|
106
|
+
tweenRef.current?.kill();
|
|
107
|
+
tweenRef.current = null;
|
|
108
|
+
node.set(native, toValueType(node, native, value));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!started) {
|
|
112
|
+
started = true;
|
|
113
|
+
applyFrom = tweenRef.current === null; // first build (mount) ⇒ apply `from`
|
|
114
|
+
tweenRef.current?.kill();
|
|
115
|
+
const t = (node as unknown as Node).createTween();
|
|
116
|
+
tweenRef.current = t;
|
|
117
|
+
if (config.transition !== undefined) t.setTrans(config.transition);
|
|
118
|
+
if (config.ease !== undefined) t.setEase(config.ease);
|
|
119
|
+
if (config.speedScale !== undefined) t.setSpeedScale(config.speedScale);
|
|
120
|
+
if (props.loops !== undefined) t.setLoops(props.loops);
|
|
121
|
+
if (props.onFinished) t.finished.connect(props.onFinished);
|
|
122
|
+
t.bindNode(node as unknown as Node);
|
|
123
|
+
t.setParallel(true);
|
|
124
|
+
props.onStart?.();
|
|
125
|
+
}
|
|
126
|
+
const tw = tweenRef.current!.tweenProperty(node, native, toValueType(node, native, value), config.duration ?? 1);
|
|
127
|
+
if (applyFrom && fromValue !== undefined) tw.from(toValueType(node, native, fromValue));
|
|
128
|
+
if (props.delay !== undefined) tw.setDelay(props.delay);
|
|
129
|
+
if (config.easing) tw.setCustomInterpolator(config.easing);
|
|
130
|
+
};
|
|
131
|
+
spring[key as keyof T] = assigner as unknown as T[keyof T];
|
|
132
|
+
}
|
|
133
|
+
return spring;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useTween<T extends Record<string, TweenValue>>(
|
|
137
|
+
create: () => TweenProps<T>,
|
|
138
|
+
deps: unknown[] = [],
|
|
139
|
+
): [Spring<T>, React.RefObject<Tween | null>] {
|
|
140
|
+
const tweenRef = useRef<Tween | null>(null);
|
|
141
|
+
// 'create' is intentionally re-evaluated via `deps`, not listed here.
|
|
142
|
+
/* oxlint-disable-next-line react-hooks/exhaustive-deps */
|
|
143
|
+
const spring = useMemo(() => makeSpring(tweenRef, create()), deps);
|
|
144
|
+
// No unmount kill: the tween is `createTween()`-bound to the node, so Godot
|
|
145
|
+
// kills it when the node exits the tree. A `useEffect` cleanup would also run
|
|
146
|
+
// on StrictMode's synthetic mount teardown and kill the just-created tween.
|
|
147
|
+
return [spring, tweenRef];
|
|
148
|
+
}
|
package/src/react-jsx.ts
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import { jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
|
|
6
|
-
import type * as ReactJSX from 'react/jsx-runtime';
|
|
7
|
-
import { Fragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
|
|
8
|
-
import type { ComponentProps,
|
|
9
|
-
import { GodotVar } from '@ringozz/godot/runtime';
|
|
10
|
-
|
|
11
|
-
export function jsx(type: any, props?: any, key?: any) {
|
|
12
|
-
return reactJsx(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function jsxs(type: any, props?: any, key?: any) {
|
|
16
|
-
return reactJsxs(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function jsxDEV(type: any, props?: any, key?: any, isStatic?: any, source?: any, self?: any) {
|
|
20
|
-
return reactJsxDEV(GodotVar.isPrototypeOf(type) ? type.name : type, props, key, isStatic, source, self);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export { Fragment };
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
|
|
6
|
+
import type * as ReactJSX from 'react/jsx-runtime';
|
|
7
|
+
import { Fragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
|
|
8
|
+
import type { ComponentProps, GodotConstructor } from './react-types.ts';
|
|
9
|
+
import { GodotVar } from '@ringozz/godot/runtime';
|
|
10
|
+
|
|
11
|
+
export function jsx(type: any, props?: any, key?: any) {
|
|
12
|
+
return reactJsx(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function jsxs(type: any, props?: any, key?: any) {
|
|
16
|
+
return reactJsxs(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function jsxDEV(type: any, props?: any, key?: any, isStatic?: any, source?: any, self?: any) {
|
|
20
|
+
return reactJsxDEV(GodotVar.isPrototypeOf(type) ? type.name : type, props, key, isStatic, source, self);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { Fragment };
|
|
24
|
+
|
|
25
|
+
// JSX namespace augmentation: these types are the public JSX contract for the
|
|
26
|
+
// `@ringozz/react-godot/jsx-runtime` import source, consumed by TypeScript in
|
|
27
|
+
// consumer files. They are intentionally not referenced within this module.
|
|
28
|
+
/* oxlint-disable no-unused-vars */
|
|
29
|
+
declare module '@ringozz/react-godot/jsx-runtime' {
|
|
30
|
+
export namespace JSX {
|
|
31
|
+
interface IntrinsicAttributes extends ReactJSX.JSX.IntrinsicAttributes { }
|
|
32
|
+
interface IntrinsicElements extends ReactJSX.JSX.IntrinsicElements { }
|
|
33
|
+
interface Element extends ReactJSX.JSX.Element { }
|
|
34
|
+
|
|
35
|
+
type ElementType =
|
|
36
|
+
| ReactJSX.JSX.ElementType
|
|
37
|
+
| GodotConstructor;
|
|
38
|
+
|
|
39
|
+
type LibraryManagedAttributes<C, P> =
|
|
40
|
+
C extends GodotConstructor
|
|
41
|
+
? ComponentProps<C>
|
|
42
|
+
: ReactJSX.JSX.LibraryManagedAttributes<C, P>;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/* oxlint-enable no-unused-vars */
|
package/src/react-types.ts
CHANGED
|
@@ -4,33 +4,49 @@
|
|
|
4
4
|
|
|
5
5
|
import type React from 'react';
|
|
6
6
|
import type { Object } from '@ringozz/godot/Object';
|
|
7
|
-
import type { ValueTypes } from '@ringozz/godot';
|
|
7
|
+
import type { Signal, ValueTypes } from '@ringozz/godot';
|
|
8
|
+
import type { PackedScene } from '@ringozz/godot/PackedScene';
|
|
9
|
+
|
|
10
|
+
// Identity check sensitive to readonly (deferred-conditional trick; the
|
|
11
|
+
// comparison lives in return-type position, so it is independent of
|
|
12
|
+
// strictFunctionTypes).
|
|
13
|
+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends
|
|
14
|
+
(<T>() => T extends Y ? 1 : 2) ? true : false;
|
|
8
15
|
|
|
9
|
-
type FunctionKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
|
|
10
|
-
type Properties<T> = Omit<T, FunctionKeys<T>>;
|
|
11
|
-
type Overwrite<P, O> = Properties<P> & O;
|
|
12
|
-
type Mutable<P> = { [K in keyof P]: P[K] | Readonly<P[K]> };
|
|
13
16
|
type ConstructorRepresentation<T = any> = new (...args: any[]) => T;
|
|
14
17
|
|
|
15
18
|
export type Instance = Object;
|
|
16
19
|
|
|
20
|
+
export type GodotConstructor<T extends Instance = Instance> = ConstructorRepresentation<T> & Function;
|
|
21
|
+
|
|
17
22
|
export type InstanceProps<T extends Instance = Instance> = {
|
|
18
23
|
/** An existing instance to render instead of creating a new one. */
|
|
19
|
-
object?: T;
|
|
24
|
+
object?: T | PackedScene;
|
|
20
25
|
/** Attaches the element to a named property of the parent instead of adding it as a child. */
|
|
21
26
|
attach?: string;
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
type ReactProps<P> = React.PropsWithChildren<React.RefAttributes<P>>;
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// Settable, non-method keys. `Pick<T, K>` preserves the readonly modifier, so
|
|
32
|
+
// getter-only (read-only) props, e.g. Node.multiplayer, compare unequal to
|
|
33
|
+
// their `-readonly` copy and drop out; write-only accessors pass through with
|
|
34
|
+
// their setter parameter type.
|
|
35
|
+
type SettableKey<T, K extends keyof T> =
|
|
36
|
+
Equal<Pick<T, K>, { -readonly [P in K]: T[K] }> extends true
|
|
37
|
+
? (T[K] extends Function ? never : K)
|
|
38
|
+
: never;
|
|
39
|
+
|
|
40
|
+
// JSX props are settable instance members, typed as the setter accepts them:
|
|
41
|
+
// ValueTypes also accept JSX arrays, and signals (getter `Signal<CB>`, setter
|
|
42
|
+
// `CB | null`) take the callback directly. Maps over `keyof T` (homomorphic)
|
|
43
|
+
// so optional/readonly modifiers survive.
|
|
44
|
+
type Properties<T> = {
|
|
45
|
+
[K in keyof T as SettableKey<T, K>]:
|
|
46
|
+
T[K] extends ValueTypes ? T[K] | number[]
|
|
47
|
+
: T[K] extends Signal<infer CB> ? CB | null
|
|
48
|
+
: T[K];
|
|
28
49
|
};
|
|
29
50
|
|
|
30
|
-
type
|
|
31
|
-
|
|
32
|
-
>;
|
|
33
|
-
|
|
34
|
-
export type ComponentProps<T extends ConstructorRepresentation> = Mutable<
|
|
35
|
-
Overwrite<ElementProps<T>, Omit<InstanceProps<InstanceType<T>>, 'object'>>
|
|
36
|
-
>;
|
|
51
|
+
export type ComponentProps<T extends GodotConstructor, P extends Instance = InstanceType<T>> =
|
|
52
|
+
Partial<Properties<P> & ReactProps<P>> & InstanceProps<P>;
|