Skip to content

Commit b24bc49

Browse files
committed
chore: test new analyzer
1 parent 7a880ed commit b24bc49

File tree

2 files changed

+295
-10
lines changed

2 files changed

+295
-10
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { existsSync, readdirSync } from 'node:fs';
2+
import { dirname, join, resolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import ts from 'typescript';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = dirname(__filename);
8+
9+
// Cache for the analyzed types
10+
let threeTypesCache = null;
11+
12+
// Find the @types/three package directory
13+
const findThreeTypesDir = () => {
14+
const possiblePaths = [
15+
resolve(__dirname, '../../../node_modules/@types/three'),
16+
resolve(__dirname, '../../../node_modules/three/types'),
17+
];
18+
19+
for (const path of possiblePaths) {
20+
if (existsSync(path)) {
21+
return path;
22+
}
23+
}
24+
25+
throw new Error('Could not find Three.js type definitions');
26+
};
27+
28+
// Get all TypeScript definition files in the Three.js types directory
29+
function getAllTypeFiles(baseDir) {
30+
const result = [];
31+
32+
function scanDir(dir) {
33+
const entries = readdirSync(dir, { withFileTypes: true });
34+
35+
for (const entry of entries) {
36+
const fullPath = join(dir, entry.name);
37+
38+
if (entry.isDirectory()) {
39+
scanDir(fullPath);
40+
} else if (entry.name.endsWith('.d.ts')) {
41+
result.push(fullPath);
42+
}
43+
}
44+
}
45+
46+
scanDir(baseDir);
47+
return result;
48+
}
49+
50+
// Extract properties from a type
51+
function getPropertiesFromType(type, checker) {
52+
const properties = new Map();
53+
54+
// Get symbol and properties
55+
const symbol = type.symbol;
56+
if (!symbol) return properties;
57+
58+
// Get properties from this type
59+
const typeProperties = checker.getPropertiesOfType(type);
60+
for (const prop of typeProperties) {
61+
const propName = prop.getName();
62+
63+
// Skip methods and private properties
64+
if (propName.startsWith('_')) continue;
65+
66+
const declarations = prop.getDeclarations();
67+
if (!declarations || declarations.length === 0) continue;
68+
69+
const declaration = declarations[0];
70+
if (ts.isMethodDeclaration(declaration) || ts.isMethodSignature(declaration)) continue;
71+
72+
// Get the type of the property
73+
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
74+
const typeString = checker.typeToString(propType);
75+
76+
properties.set(propName, {
77+
name: propName,
78+
type: mapTypeToSimpleType(typeString),
79+
typeString,
80+
description: `Property from type definition: ${typeString}`,
81+
});
82+
}
83+
84+
// Get properties from base types
85+
const baseTypes = type.getBaseTypes() || [];
86+
for (const baseType of baseTypes) {
87+
const baseProperties = getPropertiesFromType(baseType, checker);
88+
for (const [name, info] of baseProperties) {
89+
if (!properties.has(name)) {
90+
properties.set(name, info);
91+
}
92+
}
93+
}
94+
95+
return properties;
96+
}
97+
98+
// Map TypeScript types to simpler types for our metadata
99+
function mapTypeToSimpleType(typeString) {
100+
if (typeString.includes('number')) return 'number';
101+
if (typeString.includes('string')) return 'string';
102+
if (typeString.includes('boolean')) return 'boolean';
103+
if (typeString.includes('Vector2')) return 'THREE.Vector2';
104+
if (typeString.includes('Vector3')) return 'THREE.Vector3';
105+
if (typeString.includes('Vector4')) return 'THREE.Vector4';
106+
if (typeString.includes('Color')) return 'THREE.Color';
107+
if (typeString.includes('Euler')) return 'THREE.Euler';
108+
if (typeString.includes('Matrix3')) return 'THREE.Matrix3';
109+
if (typeString.includes('Matrix4')) return 'THREE.Matrix4';
110+
if (typeString.includes('Quaternion')) return 'THREE.Quaternion';
111+
if (typeString.includes('Material')) return 'THREE.Material';
112+
if (typeString.includes('Texture')) return 'THREE.Texture';
113+
if (typeString.includes('Object3D')) return 'THREE.Object3D';
114+
if (typeString.includes('BufferGeometry')) return 'THREE.BufferGeometry';
115+
if (typeString.includes('BufferAttribute')) return 'THREE.BufferAttribute';
116+
if (typeString.includes('Array') || typeString.includes('[]')) return 'array';
117+
return 'any';
118+
}
119+
120+
// Analyze Three.js types and extract properties
121+
export function analyzeThreeTypes() {
122+
// Return cached result if available
123+
if (threeTypesCache) {
124+
return threeTypesCache;
125+
}
126+
127+
console.log('Analyzing Three.js type definitions...');
128+
129+
const threeTypesDir = findThreeTypesDir();
130+
const typeFiles = getAllTypeFiles(threeTypesDir);
131+
132+
// Create a TypeScript program with all type files
133+
const program = ts.createProgram(typeFiles, {
134+
target: ts.ScriptTarget.ES2020,
135+
module: ts.ModuleKind.ESNext,
136+
});
137+
138+
const checker = program.getTypeChecker();
139+
const threeClasses = new Map();
140+
141+
// Process each source file
142+
for (const sourceFile of program.getSourceFiles()) {
143+
if (!sourceFile) continue;
144+
if (
145+
!sourceFile.fileName.includes('node_modules/@types/three') &&
146+
!sourceFile.fileName.includes('node_modules/three/types')
147+
) {
148+
continue;
149+
}
150+
151+
// Process top-level declarations
152+
ts.forEachChild(sourceFile, (node) => {
153+
// Handle class declarations
154+
if (ts.isClassDeclaration(node) && node.name) {
155+
const className = node.name.text;
156+
const classType = checker.getTypeAtLocation(node);
157+
const properties = getPropertiesFromType(classType, checker);
158+
159+
threeClasses.set(className, Array.from(properties.values()));
160+
}
161+
// Handle interface declarations
162+
else if (ts.isInterfaceDeclaration(node) && node.name) {
163+
const interfaceName = node.name.text;
164+
const interfaceType = checker.getTypeAtLocation(node);
165+
const properties = getPropertiesFromType(interfaceType, checker);
166+
167+
threeClasses.set(interfaceName, Array.from(properties.values()));
168+
}
169+
// Handle namespace/module declarations (for THREE namespace)
170+
else if (
171+
ts.isModuleDeclaration(node) &&
172+
node.name.kind === ts.SyntaxKind.Identifier &&
173+
node.name.text === 'THREE'
174+
) {
175+
processThreeNamespace(node);
176+
}
177+
});
178+
}
179+
180+
function processThreeNamespace(node) {
181+
// Get the module body
182+
const body = node.body;
183+
if (!body || !ts.isModuleBlock(body)) return;
184+
185+
// Process each declaration in the module
186+
body.statements.forEach((statement) => {
187+
if (ts.isClassDeclaration(statement) && statement.name) {
188+
const className = statement.name.text;
189+
const classType = checker.getTypeAtLocation(statement);
190+
const properties = getPropertiesFromType(classType, checker);
191+
192+
threeClasses.set(className, Array.from(properties.values()));
193+
} else if (ts.isInterfaceDeclaration(statement) && statement.name) {
194+
const interfaceName = statement.name.text;
195+
const interfaceType = checker.getTypeAtLocation(statement);
196+
const properties = getPropertiesFromType(interfaceType, checker);
197+
198+
threeClasses.set(interfaceName, Array.from(properties.values()));
199+
}
200+
});
201+
}
202+
203+
// Cache the result
204+
threeTypesCache = threeClasses;
205+
console.log(`Analyzed ${threeClasses.size} Three.js classes and interfaces`);
206+
207+
return threeClasses;
208+
}
209+
210+
// Export a function to get properties for a specific Three class
211+
export function getThreeClassProperties(className) {
212+
const allClasses = analyzeThreeTypes();
213+
return allClasses.get(className) || [];
214+
}
215+
216+
// Export a function to get all Three classes and their properties
217+
export function getAllThreeClassProperties() {
218+
return analyzeThreeTypes();
219+
}

tools/scripts/json/generate.mjs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Vector4,
3232
WebGLRenderTarget,
3333
} from 'three';
34+
import { getThreeClassProperties } from './analyze-three-types.mjs';
3435
import { COMMON_ATTRIBUTES, COMMON_EVENTS, OBJECT3D_EVENTS } from './common-attributes.mjs';
3536
import { EVENT_TYPE_MAP } from './event-types.mjs';
3637
import { MATH_TYPE_MAP } from './math-types.mjs';
@@ -257,8 +258,18 @@ function buildThreeElementsMap() {
257258
accepts: prop.mathType ? prop.accepts : undefined,
258259
}));
259260

260-
if (object3DProperties.every((prop) => prop.name !== 'raycast')) {
261-
object3DProperties.push(
261+
// Get static type properties for Object3D
262+
const object3DTypeProperties = getThreeClassProperties('Object3D').map((prop) => ({
263+
name: prop.name,
264+
type: prop.type,
265+
description: `Property from type definition: ${prop.typeString}`,
266+
}));
267+
268+
// Merge runtime and static properties
269+
const mergedObject3DProperties = mergeProperties(object3DProperties, object3DTypeProperties);
270+
271+
if (mergedObject3DProperties.every((prop) => prop.name !== 'raycast')) {
272+
mergedObject3DProperties.push(
262273
{ name: 'raycast', type: 'THREE.Raycaster', accepts: ['THREE.Raycaster'] },
263274
{ name: '[raycast]', type: 'THREE.Raycaster', accepts: ['THREE.Raycaster'] },
264275
);
@@ -273,42 +284,78 @@ function buildThreeElementsMap() {
273284
try {
274285
const ThreeClass = THREE[threeName];
275286
let instance;
287+
let runtimeProperties = [];
276288

277289
try {
278290
instance = new ThreeClass();
291+
runtimeProperties = analyzeInstanceProperties(instance, ThreeClass);
279292
} catch (e) {
280-
// If we can't instantiate, check if we have known properties
293+
console.log(`Could not instantiate ${threeName}, using only type information`);
294+
}
295+
296+
// Get static type properties
297+
const typeProperties = getThreeClassProperties(threeName).map((prop) => ({
298+
...prop,
299+
name: prop.name,
300+
type: prop.type,
301+
description: `Property from type definition: ${prop.typeString}`,
302+
}));
303+
304+
// If we couldn't instantiate, check if we have known properties
305+
if (!instance) {
281306
const knownProps = KNOWN_PROPERTIES[threeName];
282307
if (knownProps) {
283308
const properties = [...knownProps.properties];
284309
if (knownProps.isObject3D) {
285-
properties.push(...object3DProperties.map((prop) => prop.name));
310+
properties.push(...mergedObject3DProperties.map((prop) => prop.name));
286311
}
287312
map.set(elementName, {
288313
threeName,
289314
isObject3D: knownProps.isObject3D,
290315
properties: properties.map((prop) => {
291-
const object3DProp = object3DProperties.find((p) => p.name === prop);
316+
const object3DProp = mergedObject3DProperties.find((p) => p.name === prop);
292317
if (object3DProp) {
293318
return object3DProp;
294319
}
320+
321+
// Look for the property in type properties
322+
const typeProp = typeProperties.find((p) => p.name === prop);
323+
if (typeProp) {
324+
return typeProp;
325+
}
326+
295327
return { name: prop, type: 'any' };
296328
}),
297329
});
298330
} else {
299-
console.warn(`Could not instantiate ${threeName} and no known properties found:`, e);
331+
// If we have type properties but no known properties, use the type properties
332+
if (typeProperties.length > 0) {
333+
const isObject3D = typeProperties.some(
334+
(prop) => prop.name === 'isObject3D' && prop.type === 'boolean',
335+
);
336+
337+
map.set(elementName, {
338+
threeName,
339+
isObject3D,
340+
properties: isObject3D ? [...typeProperties, ...mergedObject3DProperties] : typeProperties,
341+
});
342+
} else {
343+
console.warn(`Could not process ${threeName}: no instance or type information available`);
344+
}
300345
}
301346
continue;
302347
}
303348

304349
const isObject3D = instance instanceof Object3D;
305-
const properties = analyzeInstanceProperties(instance, ThreeClass);
306350

307-
if (isObject3D) {
308-
properties.push(...object3DProperties);
351+
// Merge runtime and type properties
352+
const mergedProperties = mergeProperties(runtimeProperties, typeProperties);
353+
354+
if (isObject3D && threeName !== 'Object3D') {
355+
mergedProperties.push(...mergedObject3DProperties);
309356
}
310357

311-
map.set(elementName, { threeName, isObject3D, properties });
358+
map.set(elementName, { threeName, isObject3D, properties: mergedProperties });
312359
} catch (e) {
313360
console.warn(`Could not process ${threeName}:`, e);
314361
continue;
@@ -375,6 +422,25 @@ function generateMetadata(threeMap) {
375422
});
376423
}
377424

425+
// Add this helper function to merge properties from different sources
426+
function mergeProperties(runtimeProps, typeProps) {
427+
const merged = new Map();
428+
429+
// Add runtime properties
430+
runtimeProps.forEach((prop) => {
431+
merged.set(prop.name, prop);
432+
});
433+
434+
// Add type properties that don't exist in runtime
435+
typeProps.forEach((prop) => {
436+
if (!merged.has(prop.name)) {
437+
merged.set(prop.name, prop);
438+
}
439+
});
440+
441+
return Array.from(merged.values());
442+
}
443+
378444
function generateFiles() {
379445
const threeMap = buildThreeElementsMap();
380446
const elements = generateMetadata(threeMap);

0 commit comments

Comments
 (0)