@volter/editor-react 0.5.66
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/LICENSE +661 -0
- package/NOTICE +4 -0
- package/contributions/component-contracts.service.ts +18 -0
- package/dist-node/component-prop-worker.mjs +335 -0
- package/dist-node/serving.mjs +5842 -0
- package/package.json +51 -0
- package/serving/component-prop-worker.ts +72 -0
- package/serving/index.ts +21 -0
- package/serving/ui-oid-plugin.ts +1844 -0
- package/src/source/component-prop-types.ts +517 -0
- package/src/source/ensure-import.ts +132 -0
- package/src/source/oid-transform.ts +1667 -0
- package/src/source/plan-csf-story.ts +196 -0
- package/src/source/plan-extract-component.ts +421 -0
- package/src/source/plan-fork-component.ts +621 -0
- package/src/source/plan-named-style.ts +95 -0
- package/src/source/plan-source-edit.ts +353 -0
- package/src/source/r3f-contract-resolver.ts +327 -0
- package/src/source/r3f-diagnostic-index.ts +165 -0
- package/src/source/r3f-environment-binding.ts +106 -0
- package/src/source/r3f-joint-binding.ts +228 -0
- package/src/source/r3f-lod-binding.ts +81 -0
- package/src/source/r3f-particle-binding.ts +213 -0
- package/src/source/r3f-physics-binding.ts +454 -0
- package/src/source/r3f-project-contracts.ts +444 -0
- package/src/source/relative-import-specifier.ts +34 -0
- package/src/source/reparent-guard.ts +531 -0
- package/src/source/source-edit-request.ts +22 -0
- package/src/source/syntactic-prop-specs.ts +296 -0
- package/src/source/ts-ast.ts +171 -0
- package/src/source/utility-class-support.ts +169 -0
- package/src/source/write-component-default.ts +133 -0
- package/src/source/writer.ts +1982 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source addresses for native `@react-three/rapier` impulse joints.
|
|
3
|
+
*
|
|
4
|
+
* A joint hook has no JSX node of its own, so it cannot carry an OID. The two
|
|
5
|
+
* `<RigidBody ref={…}>` elements are its existing source anchors. This analysis
|
|
6
|
+
* joins a package-recognized hook to those elements inside the same lexical
|
|
7
|
+
* component and records only JSON-shaped data on their OID entries. Literal
|
|
8
|
+
* hook parameters can be written back in place; computed parameters remain
|
|
9
|
+
* identifiable but deliberately read-only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ts from 'typescript';
|
|
13
|
+
import { enclosingScope, numericLiteral, refIdentifier } from './ts-ast';
|
|
14
|
+
|
|
15
|
+
import type { R3fJointBinding, R3fJointHook, R3fJointLiteral, R3fJointLiteralRange } from '@volter/editor-sdk/source-authoring';
|
|
16
|
+
export type { R3fJointBinding, R3fJointHook, R3fJointLiteral, R3fJointLiteralRange } from '@volter/editor-sdk/source-authoring';
|
|
17
|
+
|
|
18
|
+
const JOINT_HOOKS = new Set<R3fJointHook>([
|
|
19
|
+
'useFixedJoint',
|
|
20
|
+
'useSphericalJoint',
|
|
21
|
+
'useRevoluteJoint',
|
|
22
|
+
'usePrismaticJoint',
|
|
23
|
+
'useRopeJoint',
|
|
24
|
+
'useSpringJoint',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function rapierNamedImports(statement: ts.Statement): readonly ts.ImportSpecifier[] {
|
|
28
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
if (statement.moduleSpecifier.text !== '@react-three/rapier') return [];
|
|
32
|
+
const bindings = statement.importClause?.namedBindings;
|
|
33
|
+
return bindings && ts.isNamedImports(bindings) ? bindings.elements : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function imports(sf: ts.SourceFile): {
|
|
37
|
+
hooks: Map<string, R3fJointHook>;
|
|
38
|
+
rigidBodies: Set<string>;
|
|
39
|
+
} {
|
|
40
|
+
const hooks = new Map<string, R3fJointHook>();
|
|
41
|
+
const rigidBodies = new Set<string>();
|
|
42
|
+
for (const statement of sf.statements) {
|
|
43
|
+
for (const element of rapierNamedImports(statement)) {
|
|
44
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
45
|
+
if (imported === 'RigidBody') rigidBodies.add(element.name.text);
|
|
46
|
+
if (JOINT_HOOKS.has(imported as R3fJointHook)) {
|
|
47
|
+
hooks.set(element.name.text, imported as R3fJointHook);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { hooks, rigidBodies };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type BodyIndex = Map<ts.FunctionLikeDeclaration | null, Map<string, ts.JsxOpeningLikeElement[]>>;
|
|
55
|
+
|
|
56
|
+
interface JointBindingPair {
|
|
57
|
+
readonly first: ts.JsxOpeningLikeElement;
|
|
58
|
+
readonly second: ts.JsxOpeningLikeElement;
|
|
59
|
+
readonly binding: Omit<R3fJointBinding, 'endpoint'>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function bindingPairForCall(
|
|
63
|
+
call: ts.CallExpression,
|
|
64
|
+
sf: ts.SourceFile,
|
|
65
|
+
hooks: ReadonlyMap<string, R3fJointHook>,
|
|
66
|
+
bodies: BodyIndex,
|
|
67
|
+
): JointBindingPair | null {
|
|
68
|
+
if (!ts.isIdentifier(call.expression)) return null;
|
|
69
|
+
const hook = hooks.get(call.expression.text);
|
|
70
|
+
const body1 = call.arguments[0];
|
|
71
|
+
const body2 = call.arguments[1];
|
|
72
|
+
const paramsExpression = call.arguments[2];
|
|
73
|
+
if (!hook || !body1 || !body2 || !paramsExpression) return null;
|
|
74
|
+
if (!ts.isIdentifier(body1) || !ts.isIdentifier(body2)) return null;
|
|
75
|
+
const scopeBodies = bodies.get(enclosingScope(call));
|
|
76
|
+
const first = scopeBodies?.get(body1.text);
|
|
77
|
+
const second = scopeBodies?.get(body2.text);
|
|
78
|
+
// More than one element using one ref is not a writable address.
|
|
79
|
+
if (first?.length !== 1 || second?.length !== 1) return null;
|
|
80
|
+
const literal = literalValue(paramsExpression);
|
|
81
|
+
const params = Array.isArray(literal) && validParams(hook, literal) ? literal : undefined;
|
|
82
|
+
const ranges = params ? literalRanges(paramsExpression) : undefined;
|
|
83
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(call.getStart(sf));
|
|
84
|
+
return {
|
|
85
|
+
first: first[0]!,
|
|
86
|
+
second: second[0]!,
|
|
87
|
+
binding: {
|
|
88
|
+
hook,
|
|
89
|
+
line: line + 1,
|
|
90
|
+
col: character,
|
|
91
|
+
body1Ref: body1.text,
|
|
92
|
+
body2Ref: body2.text,
|
|
93
|
+
...(params
|
|
94
|
+
? {
|
|
95
|
+
params,
|
|
96
|
+
...(Array.isArray(ranges) ? { paramRanges: ranges } : {}),
|
|
97
|
+
}
|
|
98
|
+
: {}),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function literalValue(expression: ts.Expression): R3fJointLiteral | undefined {
|
|
104
|
+
const numeric = numericLiteral(expression);
|
|
105
|
+
if (numeric !== undefined) return numeric;
|
|
106
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
107
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
108
|
+
if (!ts.isArrayLiteralExpression(expression)) return undefined;
|
|
109
|
+
const result: R3fJointLiteral[] = [];
|
|
110
|
+
for (const element of expression.elements) {
|
|
111
|
+
if (ts.isSpreadElement(element) || ts.isOmittedExpression(element)) return undefined;
|
|
112
|
+
const value = literalValue(element);
|
|
113
|
+
if (value === undefined) return undefined;
|
|
114
|
+
result.push(value);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function literalRanges(expression: ts.Expression): R3fJointLiteralRange | undefined {
|
|
120
|
+
if (numericLiteral(expression) !== undefined) {
|
|
121
|
+
return { start: expression.getStart(), end: expression.getEnd() };
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
expression.kind === ts.SyntaxKind.TrueKeyword ||
|
|
125
|
+
expression.kind === ts.SyntaxKind.FalseKeyword
|
|
126
|
+
) {
|
|
127
|
+
return { start: expression.getStart(), end: expression.getEnd() };
|
|
128
|
+
}
|
|
129
|
+
if (!ts.isArrayLiteralExpression(expression)) return undefined;
|
|
130
|
+
const result: R3fJointLiteralRange[] = [];
|
|
131
|
+
for (const element of expression.elements) {
|
|
132
|
+
if (ts.isSpreadElement(element) || ts.isOmittedExpression(element)) return undefined;
|
|
133
|
+
const range = literalRanges(element);
|
|
134
|
+
if (!range) return undefined;
|
|
135
|
+
result.push(range);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validParams(hook: R3fJointHook, params: readonly R3fJointLiteral[]): boolean {
|
|
141
|
+
const vec = (value: R3fJointLiteral | undefined, length: number): boolean =>
|
|
142
|
+
Array.isArray(value) &&
|
|
143
|
+
value.length === length &&
|
|
144
|
+
value.every((part) => typeof part === 'number');
|
|
145
|
+
switch (hook) {
|
|
146
|
+
case 'useFixedJoint':
|
|
147
|
+
return (
|
|
148
|
+
params.length === 4 &&
|
|
149
|
+
vec(params[0], 3) &&
|
|
150
|
+
vec(params[1], 4) &&
|
|
151
|
+
vec(params[2], 3) &&
|
|
152
|
+
vec(params[3], 4)
|
|
153
|
+
);
|
|
154
|
+
case 'useSphericalJoint':
|
|
155
|
+
return params.length === 2 && vec(params[0], 3) && vec(params[1], 3);
|
|
156
|
+
case 'useRevoluteJoint':
|
|
157
|
+
case 'usePrismaticJoint':
|
|
158
|
+
return (
|
|
159
|
+
(params.length === 3 || params.length === 4) &&
|
|
160
|
+
vec(params[0], 3) &&
|
|
161
|
+
vec(params[1], 3) &&
|
|
162
|
+
vec(params[2], 3) &&
|
|
163
|
+
(params.length === 3 || vec(params[3], 2))
|
|
164
|
+
);
|
|
165
|
+
case 'useRopeJoint':
|
|
166
|
+
return (
|
|
167
|
+
params.length === 3 &&
|
|
168
|
+
vec(params[0], 3) &&
|
|
169
|
+
vec(params[1], 3) &&
|
|
170
|
+
typeof params[2] === 'number'
|
|
171
|
+
);
|
|
172
|
+
case 'useSpringJoint':
|
|
173
|
+
return (
|
|
174
|
+
params.length === 5 &&
|
|
175
|
+
vec(params[0], 3) &&
|
|
176
|
+
vec(params[1], 3) &&
|
|
177
|
+
params.slice(2).every((part) => typeof part === 'number')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Joint bindings keyed by the existing `<RigidBody>` opening element. */
|
|
183
|
+
export function jointBindingsByElement(
|
|
184
|
+
sf: ts.SourceFile,
|
|
185
|
+
): Map<ts.JsxOpeningLikeElement, readonly R3fJointBinding[]> {
|
|
186
|
+
const imported = imports(sf);
|
|
187
|
+
const result = new Map<ts.JsxOpeningLikeElement, R3fJointBinding[]>();
|
|
188
|
+
if (imported.hooks.size === 0 || imported.rigidBodies.size === 0) return result;
|
|
189
|
+
|
|
190
|
+
const bodies: BodyIndex = new Map();
|
|
191
|
+
const collectBodies = (node: ts.Node): void => {
|
|
192
|
+
if (
|
|
193
|
+
(ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
|
|
194
|
+
imported.rigidBodies.has(node.tagName.getText(sf))
|
|
195
|
+
) {
|
|
196
|
+
const ref = refIdentifier(node);
|
|
197
|
+
if (ref) {
|
|
198
|
+
const scope = enclosingScope(node);
|
|
199
|
+
const scoped = bodies.get(scope) ?? new Map();
|
|
200
|
+
const matches = scoped.get(ref) ?? [];
|
|
201
|
+
matches.push(node);
|
|
202
|
+
scoped.set(ref, matches);
|
|
203
|
+
bodies.set(scope, scoped);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
ts.forEachChild(node, collectBodies);
|
|
207
|
+
};
|
|
208
|
+
collectBodies(sf);
|
|
209
|
+
|
|
210
|
+
const attach = (element: ts.JsxOpeningLikeElement, binding: R3fJointBinding): void => {
|
|
211
|
+
const bindings = result.get(element) ?? [];
|
|
212
|
+
bindings.push(binding);
|
|
213
|
+
result.set(element, bindings);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const collectHooks = (node: ts.Node): void => {
|
|
217
|
+
if (ts.isCallExpression(node)) {
|
|
218
|
+
const pair = bindingPairForCall(node, sf, imported.hooks, bodies);
|
|
219
|
+
if (pair) {
|
|
220
|
+
attach(pair.first, { ...pair.binding, endpoint: 0 });
|
|
221
|
+
attach(pair.second, { ...pair.binding, endpoint: 1 });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
ts.forEachChild(node, collectHooks);
|
|
225
|
+
};
|
|
226
|
+
collectHooks(sf);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Source addresses for Drei's native THREE.LOD projection (`<Detailed>`). */
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { jsxAttribute, numericLiteral } from './ts-ast';
|
|
5
|
+
|
|
6
|
+
import type { R3fLodBinding, R3fLodNumberBinding } from '@volter/editor-sdk/source-authoring';
|
|
7
|
+
export type { R3fLodBinding, R3fLodNumberBinding } from '@volter/editor-sdk/source-authoring';
|
|
8
|
+
|
|
9
|
+
function detailedNames(sf: ts.SourceFile): Set<string> {
|
|
10
|
+
const names = new Set<string>();
|
|
11
|
+
for (const statement of sf.statements) {
|
|
12
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!statement.moduleSpecifier.text.startsWith('@react-three/drei')) continue;
|
|
16
|
+
const bindings = statement.importClause?.namedBindings;
|
|
17
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
18
|
+
for (const element of bindings.elements) {
|
|
19
|
+
if ((element.propertyName?.text ?? element.name.text) === 'Detailed') {
|
|
20
|
+
names.add(element.name.text);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} else if (bindings && ts.isNamespaceImport(bindings)) {
|
|
24
|
+
names.add(`${bindings.name.text}.Detailed`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expressionOf(attribute: ts.JsxAttribute | undefined): ts.Expression | undefined {
|
|
31
|
+
return attribute?.initializer && ts.isJsxExpression(attribute.initializer)
|
|
32
|
+
? attribute.initializer.expression
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function numberBinding(
|
|
37
|
+
expression: ts.Expression,
|
|
38
|
+
sf: ts.SourceFile,
|
|
39
|
+
): R3fLodNumberBinding | undefined {
|
|
40
|
+
const value = numericLiteral(expression);
|
|
41
|
+
return value === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: { value, start: expression.getStart(sf), end: expression.getEnd() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Bind every recognized `<Detailed>` even when its values are computed: the
|
|
47
|
+
* live THREE.LOD remains inspectable, while only literal tokens are writable. */
|
|
48
|
+
export function lodBindingsByElement(
|
|
49
|
+
sf: ts.SourceFile,
|
|
50
|
+
): Map<ts.JsxOpeningLikeElement, R3fLodBinding> {
|
|
51
|
+
const names = detailedNames(sf);
|
|
52
|
+
const result = new Map<ts.JsxOpeningLikeElement, R3fLodBinding>();
|
|
53
|
+
const visit = (node: ts.Node): void => {
|
|
54
|
+
if (
|
|
55
|
+
(ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
|
|
56
|
+
names.has(node.tagName.getText(sf))
|
|
57
|
+
) {
|
|
58
|
+
const distancesExpression = expressionOf(jsxAttribute(node, 'distances'));
|
|
59
|
+
const distanceBindings =
|
|
60
|
+
distancesExpression && ts.isArrayLiteralExpression(distancesExpression)
|
|
61
|
+
? distancesExpression.elements.map((element) => numberBinding(element, sf))
|
|
62
|
+
: [];
|
|
63
|
+
const distances =
|
|
64
|
+
distanceBindings.length > 0 && distanceBindings.every((value) => value !== undefined)
|
|
65
|
+
? (distanceBindings as R3fLodNumberBinding[])
|
|
66
|
+
: undefined;
|
|
67
|
+
const hysteresisExpression = expressionOf(jsxAttribute(node, 'hysteresis'));
|
|
68
|
+
const hysteresis = hysteresisExpression ? numberBinding(hysteresisExpression, sf) : undefined;
|
|
69
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
70
|
+
result.set(node, {
|
|
71
|
+
line: line + 1,
|
|
72
|
+
col: character,
|
|
73
|
+
...(distances ? { distances } : {}),
|
|
74
|
+
...(hysteresis ? { hysteresis } : {}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
ts.forEachChild(node, visit);
|
|
78
|
+
};
|
|
79
|
+
visit(sf);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/** Source address for a native three.quarks `ParticleSystem` emitter shape. */
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { enclosingScope, numericLiteral } from './ts-ast';
|
|
5
|
+
|
|
6
|
+
import type { QuarksEmitterShape, R3fParticleBinding, R3fParticleNumberBinding } from '@volter/editor-sdk/source-authoring';
|
|
7
|
+
export type { QuarksEmitterShape, R3fParticleBinding, R3fParticleNumberBinding } from '@volter/editor-sdk/source-authoring';
|
|
8
|
+
|
|
9
|
+
const SHAPES: Record<string, QuarksEmitterShape> = {
|
|
10
|
+
PointEmitter: 'point',
|
|
11
|
+
SphereEmitter: 'sphere',
|
|
12
|
+
HemisphereEmitter: 'hemisphere',
|
|
13
|
+
ConeEmitter: 'cone',
|
|
14
|
+
CircleEmitter: 'circle',
|
|
15
|
+
DonutEmitter: 'donut',
|
|
16
|
+
RectangleEmitter: 'rectangle',
|
|
17
|
+
GridEmitter: 'grid',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface Imports {
|
|
21
|
+
readonly particleSystems: Set<string>;
|
|
22
|
+
readonly shapes: Map<string, { imported: string; shape: QuarksEmitterShape }>;
|
|
23
|
+
readonly useMemo: Set<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function namedImports(statement: ts.Statement, module: string): readonly ts.ImportSpecifier[] {
|
|
27
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
if (statement.moduleSpecifier.text !== module) return [];
|
|
31
|
+
const bindings = statement.importClause?.namedBindings;
|
|
32
|
+
return bindings && ts.isNamedImports(bindings) ? bindings.elements : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function importsOf(sf: ts.SourceFile): Imports {
|
|
36
|
+
const particleSystems = new Set<string>();
|
|
37
|
+
const shapes = new Map<string, { imported: string; shape: QuarksEmitterShape }>();
|
|
38
|
+
const useMemo = new Set<string>();
|
|
39
|
+
for (const statement of sf.statements) {
|
|
40
|
+
for (const element of namedImports(statement, 'three.quarks')) {
|
|
41
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
42
|
+
if (imported === 'ParticleSystem') particleSystems.add(element.name.text);
|
|
43
|
+
const shape = SHAPES[imported];
|
|
44
|
+
if (shape) shapes.set(element.name.text, { imported, shape });
|
|
45
|
+
}
|
|
46
|
+
for (const element of namedImports(statement, 'quarks.core')) {
|
|
47
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
48
|
+
const shape = SHAPES[imported];
|
|
49
|
+
if (shape) shapes.set(element.name.text, { imported, shape });
|
|
50
|
+
}
|
|
51
|
+
for (const element of namedImports(statement, 'react')) {
|
|
52
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
53
|
+
if (imported === 'useMemo') useMemo.add(element.name.text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { particleSystems, shapes, useMemo };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function returnedExpression(
|
|
60
|
+
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
61
|
+
): ts.Expression | undefined {
|
|
62
|
+
if (!ts.isBlock(fn.body)) return fn.body;
|
|
63
|
+
return fn.body.statements.find(ts.isReturnStatement)?.expression;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function particleSystemExpression(
|
|
67
|
+
expression: ts.Expression,
|
|
68
|
+
imported: Imports,
|
|
69
|
+
): ts.NewExpression | undefined {
|
|
70
|
+
if (
|
|
71
|
+
ts.isNewExpression(expression) &&
|
|
72
|
+
ts.isIdentifier(expression.expression) &&
|
|
73
|
+
imported.particleSystems.has(expression.expression.text)
|
|
74
|
+
) {
|
|
75
|
+
return expression;
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
!ts.isCallExpression(expression) ||
|
|
79
|
+
!ts.isIdentifier(expression.expression) ||
|
|
80
|
+
!imported.useMemo.has(expression.expression.text)
|
|
81
|
+
) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const factory = expression.arguments[0];
|
|
85
|
+
if (!factory || (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)))
|
|
86
|
+
return undefined;
|
|
87
|
+
const returned = returnedExpression(factory);
|
|
88
|
+
return returned ? particleSystemExpression(returned, imported) : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function propertyName(name: ts.PropertyName, sf: ts.SourceFile): string {
|
|
92
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)
|
|
93
|
+
? name.text
|
|
94
|
+
: name.getText(sf);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function property(
|
|
98
|
+
object: ts.ObjectLiteralExpression,
|
|
99
|
+
name: string,
|
|
100
|
+
sf: ts.SourceFile,
|
|
101
|
+
): ts.PropertyAssignment | undefined {
|
|
102
|
+
return object.properties.find(
|
|
103
|
+
(candidate): candidate is ts.PropertyAssignment =>
|
|
104
|
+
ts.isPropertyAssignment(candidate) && propertyName(candidate.name, sf) === name,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function bindingOf(
|
|
109
|
+
system: string,
|
|
110
|
+
declaration: ts.VariableDeclaration,
|
|
111
|
+
sf: ts.SourceFile,
|
|
112
|
+
imported: Imports,
|
|
113
|
+
): R3fParticleBinding | undefined {
|
|
114
|
+
const initializer = declaration.initializer;
|
|
115
|
+
if (!initializer) return undefined;
|
|
116
|
+
const construction = particleSystemExpression(initializer, imported);
|
|
117
|
+
const options = construction?.arguments?.[0];
|
|
118
|
+
if (!options || !ts.isObjectLiteralExpression(options)) return undefined;
|
|
119
|
+
const shapeProperty = property(options, 'shape', sf);
|
|
120
|
+
const shapeExpression = shapeProperty?.initializer;
|
|
121
|
+
if (
|
|
122
|
+
!shapeExpression ||
|
|
123
|
+
!ts.isNewExpression(shapeExpression) ||
|
|
124
|
+
!ts.isIdentifier(shapeExpression.expression)
|
|
125
|
+
) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const shape = imported.shapes.get(shapeExpression.expression.text);
|
|
129
|
+
if (!shape) return undefined;
|
|
130
|
+
const parameters = shapeExpression.arguments?.[0];
|
|
131
|
+
const fields: Record<string, R3fParticleNumberBinding> = {};
|
|
132
|
+
if (parameters && ts.isObjectLiteralExpression(parameters)) {
|
|
133
|
+
for (const candidate of parameters.properties) {
|
|
134
|
+
if (!ts.isPropertyAssignment(candidate)) continue;
|
|
135
|
+
const value = numericLiteral(candidate.initializer);
|
|
136
|
+
if (value === undefined) continue;
|
|
137
|
+
fields[propertyName(candidate.name, sf)] = {
|
|
138
|
+
value,
|
|
139
|
+
start: candidate.initializer.getStart(sf),
|
|
140
|
+
end: candidate.initializer.getEnd(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(shapeExpression.getStart(sf));
|
|
145
|
+
return {
|
|
146
|
+
system,
|
|
147
|
+
shape: shape.shape,
|
|
148
|
+
constructor: shape.imported,
|
|
149
|
+
line: line + 1,
|
|
150
|
+
col: character,
|
|
151
|
+
fields,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function primitiveSystem(element: ts.JsxOpeningLikeElement): string | undefined {
|
|
156
|
+
if (element.tagName.getText() !== 'primitive') return undefined;
|
|
157
|
+
const object = element.attributes.properties.find(
|
|
158
|
+
(candidate): candidate is ts.JsxAttribute =>
|
|
159
|
+
ts.isJsxAttribute(candidate) && candidate.name.getText() === 'object',
|
|
160
|
+
);
|
|
161
|
+
const expression =
|
|
162
|
+
object?.initializer && ts.isJsxExpression(object.initializer)
|
|
163
|
+
? object.initializer.expression
|
|
164
|
+
: undefined;
|
|
165
|
+
return expression &&
|
|
166
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
167
|
+
ts.isIdentifier(expression.expression) &&
|
|
168
|
+
expression.name.text === 'emitter'
|
|
169
|
+
? expression.expression.text
|
|
170
|
+
: undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Native particle bindings keyed by `<primitive object={system.emitter}>`. */
|
|
174
|
+
export function particleBindingsByElement(
|
|
175
|
+
sf: ts.SourceFile,
|
|
176
|
+
): Map<ts.JsxOpeningLikeElement, R3fParticleBinding> {
|
|
177
|
+
const imported = importsOf(sf);
|
|
178
|
+
const byScope = new Map<ts.FunctionLikeDeclaration | null, Map<string, R3fParticleBinding>>();
|
|
179
|
+
const collectSystems = (node: ts.Node): void => {
|
|
180
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
181
|
+
const binding = bindingOf(node.name.text, node, sf, imported);
|
|
182
|
+
if (binding) {
|
|
183
|
+
const scope = enclosingScope(node);
|
|
184
|
+
const systems = byScope.get(scope) ?? new Map();
|
|
185
|
+
systems.set(node.name.text, binding);
|
|
186
|
+
byScope.set(scope, systems);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
ts.forEachChild(node, collectSystems);
|
|
190
|
+
};
|
|
191
|
+
collectSystems(sf);
|
|
192
|
+
|
|
193
|
+
const result = new Map<ts.JsxOpeningLikeElement, R3fParticleBinding>();
|
|
194
|
+
const resolve = (node: ts.Node, system: string): R3fParticleBinding | undefined => {
|
|
195
|
+
let scope: ts.FunctionLikeDeclaration | null = enclosingScope(node);
|
|
196
|
+
for (;;) {
|
|
197
|
+
const binding = byScope.get(scope)?.get(system);
|
|
198
|
+
if (binding) return binding;
|
|
199
|
+
if (scope === null) return undefined;
|
|
200
|
+
scope = enclosingScope(scope);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const join = (node: ts.Node): void => {
|
|
204
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
205
|
+
const system = primitiveSystem(node);
|
|
206
|
+
const binding = system ? resolve(node, system) : undefined;
|
|
207
|
+
if (binding) result.set(node, binding);
|
|
208
|
+
}
|
|
209
|
+
ts.forEachChild(node, join);
|
|
210
|
+
};
|
|
211
|
+
join(sf);
|
|
212
|
+
return result;
|
|
213
|
+
}
|