@volter/editor-react 0.5.210 → 0.5.212
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/contributions/component-contracts.service.ts +1 -1
- package/dist-node/serving.mjs +14770 -1629
- package/package.json +20 -9
- package/serving/component-prop-worker.ts +3 -19
- package/serving/index.ts +20 -8
- package/serving/source-diagnostics.ts +311 -0
- package/serving/ui-oid-plugin.ts +154 -775
- package/src/source/component-prop-types.ts +6 -62
- package/src/source/drei-detailed-names.ts +24 -0
- package/src/source/oid-transform.ts +166 -376
- package/src/source/plan-csf-story.ts +3 -26
- package/src/source/plan-extract-component.ts +15 -60
- package/src/source/plan-fork-component.ts +25 -92
- package/src/source/plan-named-style.ts +3 -17
- package/src/source/plan-source-edit.ts +9 -40
- package/src/source/r3f-contract-resolver.ts +9 -103
- package/src/source/r3f-physics-binding.ts +28 -186
- package/src/source/r3f-project-contracts.ts +13 -103
- package/src/source/relative-import-specifier.ts +3 -18
- package/src/source/reparent-guard.ts +16 -61
- package/src/source/source-edit-request.ts +2 -20
- package/src/source/ts-ast.ts +5 -95
- package/src/source/utility-class-support.ts +4 -32
- package/src/source/writer.ts +74 -326
- package/src/source/r3f-diagnostic-index.ts +0 -165
- package/src/source/r3f-environment-binding.ts +0 -106
- package/src/source/r3f-joint-binding.ts +0 -228
- package/src/source/r3f-lod-binding.ts +0 -81
- package/src/source/r3f-particle-binding.ts +0 -213
- package/src/source/syntactic-prop-specs.ts +0 -296
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "warnings live on the row".
|
|
3
|
-
*
|
|
4
|
-
* The join that carries an R3F authorability diagnostic from where it is
|
|
5
|
-
* PRODUCED (a whole-file analysis, `r3fAuthoringDiagnostics`) to where it is
|
|
6
|
-
* READ (one hierarchy row). Pure, browser-safe, and unit-testable without a
|
|
7
|
-
* dev server: the two halves below are the entire mapping, and both the
|
|
8
|
-
* server-side annotator (`currentOidIndex`, `vite-plugin-ui-oid.ts`) and the
|
|
9
|
-
* client-side reader (`R3fSourceAuthoringAdapter.diagnosticsFor`) call one of
|
|
10
|
-
* them rather than re-deriving it.
|
|
11
|
-
*
|
|
12
|
-
* ## Why the join is by IDENTITY the index already holds, never by name
|
|
13
|
-
*
|
|
14
|
-
* Before H5 these diagnostics reached exactly one place: the dev server's
|
|
15
|
-
* terminal (and the `/__editor/state` warnings map), formatted to a string by
|
|
16
|
-
* `server/project-validation.ts`'s `validateSource`. A string in a log has no
|
|
17
|
-
* row identity at all, so the first question was what identity to give it.
|
|
18
|
-
*
|
|
19
|
-
* A diagnostic carries `{file, line, col, component}`. An `OidEntry` carries
|
|
20
|
-
* `{file, line, col, component, tag}` — where `component` is the component
|
|
21
|
-
* whose BODY lexically contains the element. Those two `component` fields mean
|
|
22
|
-
* the same thing on the definition side, and both positions come from
|
|
23
|
-
* `node.getStart()`, so the entry-side selection below needs no name
|
|
24
|
-
* resolution, no import walking, and no second derivation that could disagree
|
|
25
|
-
* with the first:
|
|
26
|
-
*
|
|
27
|
-
* - **at this element** — the diagnostic's `(file,line,col)` IS this entry's.
|
|
28
|
-
* That is R3F004 ("`<Enemy>` has no name"), whose subject is the callsite
|
|
29
|
-
* element itself. (R3F002/3/5 point at a function/variable NAME identifier
|
|
30
|
-
* or a `ref={…}` identifier, never at a `<`, so they can never collide with
|
|
31
|
-
* an element position and be misread as this kind.)
|
|
32
|
-
* - **inside this component** — the diagnostic names the component whose body
|
|
33
|
-
* contains this element (`d.component === entry.component`, same file). That
|
|
34
|
-
* is R3F002/R3F003/R3F005, which are ABOUT a definition.
|
|
35
|
-
*
|
|
36
|
-
* The second rule is what gets a definition-side warning onto an INSTANCE row
|
|
37
|
-
* in another file, and it needs no cross-file resolution: the oid transform
|
|
38
|
-
* stamps a component boundary with BOTH its callsite oid
|
|
39
|
-
* (`userData.authoringInstance`) and its definition-root oid (`userData.oid`),
|
|
40
|
-
* so the client already holds both index entries for one row — the same two
|
|
41
|
-
* entries H3's `sourceLocation`/`definitionLocation` return. {@link
|
|
42
|
-
* mergeRowDiagnostics} unions them.
|
|
43
|
-
*
|
|
44
|
-
* ## Why this cannot drift from index freshness
|
|
45
|
-
*
|
|
46
|
-
* It is not a separate channel. The diagnostics are attached to the entries of
|
|
47
|
-
* the very payload `refreshSourceState()` already fetches, computed in the same
|
|
48
|
-
* pass from the same file bytes. There is no second fetch to fall behind, no
|
|
49
|
-
* cache to invalidate, and no clock of its own: whatever makes the index stale
|
|
50
|
-
* makes them stale together, and whatever refreshes it refreshes them.
|
|
51
|
-
*
|
|
52
|
-
* ## A diagnostic that lands on no row does not vanish
|
|
53
|
-
*
|
|
54
|
-
* Some will not reach a row, and that is expected rather than a hole to be
|
|
55
|
-
* plugged: a component defined but never instantiated has no live object, a
|
|
56
|
-
* component whose definition file is not stamped as R3F is never analyzed here
|
|
57
|
-
* at all, and a hosted (no-dev-server) session has no analyzer (see
|
|
58
|
-
* `browser-oid-index.ts`). Nothing is LOST in any of those cases — this join is
|
|
59
|
-
* PURELY ADDITIVE. The channel these diagnostics have always had is untouched:
|
|
60
|
-
* `server/project-validation.ts`'s `validateSource` still formats every one of
|
|
61
|
-
* them into the dev server's terminal warning block and into the project
|
|
62
|
-
* warnings map that `/__editor/state` (and so `volter-game-editor status`) reports. Rows are
|
|
63
|
-
* a second, better-placed audience, not a replacement.
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
import type { OidEntry, R3fAuthoringDiagnostic } from './oid-transform';
|
|
67
|
-
|
|
68
|
-
/** The per-entry selector {@link fileDiagnosticJoin} builds. */
|
|
69
|
-
export type EntryDiagnosticSelector = (
|
|
70
|
-
entry: Pick<OidEntry, 'line' | 'col' | 'component' | 'parentOid'>,
|
|
71
|
-
) => R3fAuthoringDiagnostic[] | undefined;
|
|
72
|
-
|
|
73
|
-
const position = (at: { line: number; col: number }): string => `${at.line}:${at.col}`;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Build the selector for ONE file: which of that file's diagnostics pertain to
|
|
77
|
-
* each of that file's oid entries. Both arguments must belong to the same
|
|
78
|
-
* file — the caller already has them grouped that way, so nothing here
|
|
79
|
-
* re-filters by file.
|
|
80
|
-
*
|
|
81
|
-
* The two rules of the module doc are applied as a PARTITION, not as an `||`,
|
|
82
|
-
* and that distinction is load-bearing. A diagnostic whose position is one of
|
|
83
|
-
* this file's stamped elements is ABOUT that element (R3F004); everything else
|
|
84
|
-
* is about a component definition (R3F002/3/5). Without the partition, an
|
|
85
|
-
* R3F004 on `<Enemy>` in a file that ALSO defines `Enemy` would match the
|
|
86
|
-
* component rule too and badge every element inside `Enemy`'s body — a warning
|
|
87
|
-
* about one callsite smeared across the definition's internals.
|
|
88
|
-
*
|
|
89
|
-
* Deriving the split from the data (does this position name a stamped
|
|
90
|
-
* element?) rather than from a hand-kept list of which codes are
|
|
91
|
-
* callsite-scoped is deliberate: a new R3F00x code classifies itself.
|
|
92
|
-
*
|
|
93
|
-
* The component rule additionally lands on that component's ROOT element only
|
|
94
|
-
* — the outermost stamped element of its body, recognized through `parentOid`
|
|
95
|
-
* (no parent, or a parent belonging to a different component). Two reasons,
|
|
96
|
-
* one practical and one exact: a definition's internals are not what an author
|
|
97
|
-
* is being warned about, and the root element is PRECISELY the entry an
|
|
98
|
-
* instance row reaches, because that oid is what the transform stamps on the
|
|
99
|
-
* boundary object as `userData.oid`. Anything looser would badge every
|
|
100
|
-
* `<mesh>` inside the component as well.
|
|
101
|
-
*
|
|
102
|
-
* The returned selector yields a fresh array, or `undefined` when nothing
|
|
103
|
-
* pertains — so the caller omits the field entirely instead of shipping an
|
|
104
|
-
* empty array on every entry.
|
|
105
|
-
*/
|
|
106
|
-
export function fileDiagnosticJoin(
|
|
107
|
-
fileEntries: ReadonlyMap<string, OidEntry>,
|
|
108
|
-
fileDiagnostics: readonly R3fAuthoringDiagnostic[],
|
|
109
|
-
): EntryDiagnosticSelector {
|
|
110
|
-
if (fileDiagnostics.length === 0) return () => undefined;
|
|
111
|
-
const elementPositions = new Set<string>();
|
|
112
|
-
for (const [, entry] of fileEntries) elementPositions.add(position(entry));
|
|
113
|
-
|
|
114
|
-
const atElement: R3fAuthoringDiagnostic[] = [];
|
|
115
|
-
const aboutComponent: R3fAuthoringDiagnostic[] = [];
|
|
116
|
-
for (const diagnostic of fileDiagnostics) {
|
|
117
|
-
(elementPositions.has(position(diagnostic)) ? atElement : aboutComponent).push(diagnostic);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const isComponentRoot = (entry: Pick<OidEntry, 'component' | 'parentOid'>): boolean => {
|
|
121
|
-
if (entry.parentOid === undefined) return true;
|
|
122
|
-
return fileEntries.get(entry.parentOid)?.component !== entry.component;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return (entry) => {
|
|
126
|
-
const pertinent = [
|
|
127
|
-
...atElement.filter((d) => d.line === entry.line && d.col === entry.col),
|
|
128
|
-
...(entry.component === null || !isComponentRoot(entry)
|
|
129
|
-
? []
|
|
130
|
-
: aboutComponent.filter((d) => d.component === entry.component)),
|
|
131
|
-
];
|
|
132
|
-
return pertinent.length > 0 ? pertinent : undefined;
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Stable identity of one diagnostic, for the union below. Code + position is
|
|
137
|
-
* exact: two diagnostics of the same code at the same place ARE the same
|
|
138
|
-
* finding reached through two entries, which is the normal case when a
|
|
139
|
-
* component is defined and instantiated in one file. */
|
|
140
|
-
function diagnosticKey(diagnostic: R3fAuthoringDiagnostic): string {
|
|
141
|
-
return `${diagnostic.code}|${diagnostic.file}|${diagnostic.line}|${diagnostic.col}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Union of the diagnostics reachable from one ROW's index entries — its
|
|
146
|
-
* callsite entry and its definition-root entry — in that order, deduped.
|
|
147
|
-
* `undefined`/absent entries are skipped, so a row with only one of the two
|
|
148
|
-
* (a plain host element; an instance whose index entry has not landed) is the
|
|
149
|
-
* ordinary case, not a special one.
|
|
150
|
-
*/
|
|
151
|
-
export function mergeRowDiagnostics(
|
|
152
|
-
...entries: ReadonlyArray<Pick<OidEntry, 'diagnostics'> | undefined>
|
|
153
|
-
): R3fAuthoringDiagnostic[] {
|
|
154
|
-
const seen = new Set<string>();
|
|
155
|
-
const merged: R3fAuthoringDiagnostic[] = [];
|
|
156
|
-
for (const entry of entries) {
|
|
157
|
-
for (const diagnostic of entry?.diagnostics ?? []) {
|
|
158
|
-
const key = diagnosticKey(diagnostic);
|
|
159
|
-
if (seen.has(key)) continue;
|
|
160
|
-
seen.add(key);
|
|
161
|
-
merged.push(diagnostic);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return merged;
|
|
165
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/** Exact source tokens for native R3F scene attachments.
|
|
2
|
-
*
|
|
3
|
-
* These are not a Volter environment format. `<color attach="background">`,
|
|
4
|
-
* `<fog attach="fog">`, and `<fogExp2 attach="fog">` are ordinary Fiber
|
|
5
|
-
* projections of THREE.Scene properties. The live Scene remains runtime truth;
|
|
6
|
-
* this metadata only identifies literal constructor arguments that can be
|
|
7
|
-
* changed without interpreting arbitrary project code.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import ts from 'typescript';
|
|
11
|
-
import { jsxAttribute, numericLiteral } from './ts-ast';
|
|
12
|
-
|
|
13
|
-
import type { R3fEnvironmentBinding, R3fEnvironmentNumberBinding, R3fEnvironmentStringBinding } from '@volter/sdk/source-authoring';
|
|
14
|
-
export type { R3fEnvironmentBinding, R3fEnvironmentNumberBinding, R3fEnvironmentStringBinding } from '@volter/sdk/source-authoring';
|
|
15
|
-
|
|
16
|
-
function attributeString(attribute: ts.JsxAttribute | undefined): string | undefined {
|
|
17
|
-
const initializer = attribute?.initializer;
|
|
18
|
-
if (!initializer) return undefined;
|
|
19
|
-
if (ts.isStringLiteral(initializer)) return initializer.text;
|
|
20
|
-
const expression = ts.isJsxExpression(initializer) ? initializer.expression : undefined;
|
|
21
|
-
return expression &&
|
|
22
|
-
(ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression))
|
|
23
|
-
? expression.text
|
|
24
|
-
: undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function args(element: ts.JsxOpeningLikeElement): readonly ts.Expression[] | undefined {
|
|
28
|
-
const initializer = jsxAttribute(element, 'args')?.initializer;
|
|
29
|
-
const expression =
|
|
30
|
-
initializer && ts.isJsxExpression(initializer) ? initializer.expression : undefined;
|
|
31
|
-
return expression && ts.isArrayLiteralExpression(expression)
|
|
32
|
-
? expression.elements.filter(ts.isExpression)
|
|
33
|
-
: undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function numberBinding(
|
|
37
|
-
expression: ts.Expression | undefined,
|
|
38
|
-
sf: ts.SourceFile,
|
|
39
|
-
): R3fEnvironmentNumberBinding | undefined {
|
|
40
|
-
if (!expression) return undefined;
|
|
41
|
-
const value = numericLiteral(expression);
|
|
42
|
-
return value === undefined
|
|
43
|
-
? undefined
|
|
44
|
-
: { value, start: expression.getStart(sf), end: expression.getEnd() };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function stringBinding(
|
|
48
|
-
expression: ts.Expression | undefined,
|
|
49
|
-
sf: ts.SourceFile,
|
|
50
|
-
): R3fEnvironmentStringBinding | undefined {
|
|
51
|
-
if (
|
|
52
|
-
!expression ||
|
|
53
|
-
(!ts.isStringLiteral(expression) && !ts.isNoSubstitutionTemplateLiteral(expression))
|
|
54
|
-
) {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
const start = expression.getStart(sf);
|
|
58
|
-
const quote = sf.text[start];
|
|
59
|
-
if (quote !== "'" && quote !== '"' && quote !== '`') return undefined;
|
|
60
|
-
return { value: expression.text, start, end: expression.getEnd(), quote };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Bind recognized native scene attachments, including dynamic ones. Dynamic
|
|
64
|
-
* arguments deliberately produce a binding with missing tokens: the World
|
|
65
|
-
* Inspector can still identify the native mode but must keep that field
|
|
66
|
-
* read-only. */
|
|
67
|
-
export function environmentBindingsByElement(
|
|
68
|
-
sf: ts.SourceFile,
|
|
69
|
-
): Map<ts.JsxOpeningLikeElement, R3fEnvironmentBinding> {
|
|
70
|
-
const result = new Map<ts.JsxOpeningLikeElement, R3fEnvironmentBinding>();
|
|
71
|
-
const visit = (node: ts.Node): void => {
|
|
72
|
-
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
73
|
-
const tag = node.tagName.getText(sf);
|
|
74
|
-
const attachedTo = attributeString(jsxAttribute(node, 'attach'));
|
|
75
|
-
const values = args(node);
|
|
76
|
-
if (tag === 'color' && attachedTo === 'background') {
|
|
77
|
-
const color = stringBinding(values?.[0], sf);
|
|
78
|
-
result.set(node, {
|
|
79
|
-
kind: 'background-color',
|
|
80
|
-
...(color ? { color } : {}),
|
|
81
|
-
});
|
|
82
|
-
} else if (tag === 'fog' && attachedTo === 'fog') {
|
|
83
|
-
const color = stringBinding(values?.[0], sf);
|
|
84
|
-
const near = numberBinding(values?.[1], sf);
|
|
85
|
-
const far = numberBinding(values?.[2], sf);
|
|
86
|
-
result.set(node, {
|
|
87
|
-
kind: 'fog',
|
|
88
|
-
...(color ? { color } : {}),
|
|
89
|
-
...(near ? { near } : {}),
|
|
90
|
-
...(far ? { far } : {}),
|
|
91
|
-
});
|
|
92
|
-
} else if (tag === 'fogExp2' && attachedTo === 'fog') {
|
|
93
|
-
const color = stringBinding(values?.[0], sf);
|
|
94
|
-
const density = numberBinding(values?.[1], sf);
|
|
95
|
-
result.set(node, {
|
|
96
|
-
kind: 'fog-exp2',
|
|
97
|
-
...(color ? { color } : {}),
|
|
98
|
-
...(density ? { density } : {}),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
ts.forEachChild(node, visit);
|
|
103
|
-
};
|
|
104
|
-
visit(sf);
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
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/sdk/source-authoring';
|
|
16
|
-
export type { R3fJointBinding, R3fJointHook, R3fJointLiteral, R3fJointLiteralRange } from '@volter/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
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
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/sdk/source-authoring';
|
|
7
|
-
export type { R3fLodBinding, R3fLodNumberBinding } from '@volter/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
|
-
}
|