Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion editor/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
<script src="js/libs/ternjs/comment.js"></script>
<script src="js/libs/ternjs/infer.js"></script>
<script src="js/libs/ternjs/doc_comment.js"></script>
<script src="js/libs/tern-threejs/threejs.js"></script>
<script src="js/libs/signals.min.js"></script>

<script type="module">
Expand Down
33 changes: 31 additions & 2 deletions editor/js/Script.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { UIElement, UIPanel, UIText } from './libs/ui.js';
import { SetScriptValueCommand } from './commands/SetScriptValueCommand.js';
import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';

import buildThreeDefs from './libs/tern-threejs/build-defs.js';

function Script( editor ) {

const signals = editor.signals;
Expand Down Expand Up @@ -291,10 +293,35 @@ function Script( editor ) {
// tern js autocomplete

const server = new CodeMirror.TernServer( {
caseInsensitive: true,
plugins: { threejs: null }
caseInsensitive: true
} );

// The three.js API definitions are built lazily from the JSDoc in the library
// build the first time the script editor is opened.

let threeDefsRequested = false;

async function loadThreeDefs() {

if ( threeDefsRequested ) return;
threeDefsRequested = true;

try {

const url = new URL( '../build/three.core.js', document.baseURI ).href;
const source = await ( await fetch( url ) ).text();

server.server.defs.push( buildThreeDefs( source ) );
server.server.reset();

} catch ( error ) {

console.warn( 'Script: Failed to build three.js autocomplete defs.', error );

}

}

codemirror.setOption( 'extraKeys', {
'Ctrl-Space': function ( cm ) {

Expand Down Expand Up @@ -448,6 +475,8 @@ function Script( editor ) {
currentScript = script;
currentObject = object;

if ( mode === 'javascript' ) loadThreeDefs();

container.setDisplay( '' );
codemirror.setValue( source );
codemirror.clearHistory();
Expand Down
233 changes: 233 additions & 0 deletions editor/js/libs/tern-threejs/build-defs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Builds Tern definitions for the three.js API by scanning the JSDoc comments
// in a three.js build file (e.g. build/three.core.js).
//
// The build output is machine-formatted (one declaration per line, tab
// indentation, every documented symbol preceded by a `/** ... */` block), so a
// lightweight line scanner is enough — no full JS parser is required.
//
// Usage: const defs = buildDefs( sourceText );
//
// `defs` is a Tern definitions object of the shape consumed by the editor's
// TernServer: { "!name": "threejs", "THREE": { Box3: { prototype: { ... } } } }

// --- JSDoc type -> Tern type ------------------------------------------------

function mapType( raw, classes ) {

if ( ! raw ) return null;

let type = raw.trim();

// strip a single leading/trailing brace pair if present
type = type.replace( /^\{/, '' ).replace( /\}$/, '' ).trim();

// unions: Tern has no union type, take the first member
if ( type.includes( '|' ) ) type = type.split( '|' )[ 0 ].trim();

// nullable / non-nullable / rest markers
type = type.replace( /^[?!]/, '' ).replace( /^\.\.\./, '' ).trim();

// Array forms: Array<X>, Array.<X>, X[]
let m = type.match( /^Array\.?<(.+)>$/ );
if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';
m = type.match( /^(.+)\[\]$/ );
if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';

switch ( type ) {

case 'number': case 'string': case 'boolean': return type;
case 'function': case 'Function': return 'fn()';
case '*': case 'any': case 'Object': case 'object':
case 'undefined': case 'null': case 'void': return '?';

}

if ( classes.has( type ) ) return '+THREE.' + type;

return '?'; // unknown (TypedArray, external types, generics, …)

}

// --- JSDoc block parser -----------------------------------------------------

function parseDoc( lines ) {

const params = [];
let returns = null, atType = null, readonly = false;
const desc = [];

for ( let raw of lines ) {

const line = raw.replace( /^\s*\*?\s?/, '' ); // strip ` * `

const pm = line.match( /^@param\s+(\{[^}]*\})?\s*\[?([\w.]+)/ );
if ( pm ) { params.push( { type: pm[ 1 ] || null, name: pm[ 2 ].split( '.' )[ 0 ] } ); continue; }

const rm = line.match( /^@returns?\s+(\{[^}]*\})/ );
if ( rm ) { returns = rm[ 1 ]; continue; }

const tm = line.match( /^@type\s+(\{[^}]*\})/ );
if ( tm ) { atType = tm[ 1 ]; continue; }

if ( /^@readonly/.test( line ) ) { readonly = true; continue; }
if ( /^@/.test( line ) ) continue; // other tags ignored

if ( line.trim() ) desc.push( line.trim() );

}

// de-duplicate rest params that share a base name
const seen = new Set(), uniqueParams = [];
for ( const p of params ) if ( ! seen.has( p.name ) ) { seen.add( p.name ); uniqueParams.push( p ); }

return { params: uniqueParams, returns, atType, readonly, doc: desc.join( ' ' ) };

}

function fnType( doc, classes ) {

const args = doc.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' );
let t = 'fn(' + args + ')';
const ret = mapType( doc.returns, classes );
if ( ret ) t += ' -> ' + ret;
return t;

}

function entry( type, doc ) {

const e = { '!type': type };
if ( doc.doc ) e[ '!doc' ] = doc.doc;
return e;

}

// --- main scan --------------------------------------------------------------

export default function buildDefs( source ) {

const lines = source.split( '\n' );

// pass 1: collect class names (so types can resolve to +THREE.X)
const classes = new Set();
for ( const line of lines ) {

const m = line.match( /^class\s+(\w+)/ );
if ( m ) classes.add( m[ 1 ] );

}

const THREE = {};
let cur = null; // current class def object
let curName = null; // current class name
let pending = null; // most recent parsed JSDoc block

for ( let i = 0; i < lines.length; i ++ ) {

const line = lines[ i ];

// JSDoc block: collect then resolve against the next code line below
const t = line.trim();
if ( t.startsWith( '/**' ) ) {

const block = [];
if ( ! t.endsWith( '*/' ) ) {

i ++;
for ( ; i < lines.length; i ++ ) {

if ( lines[ i ].trim().endsWith( '*/' ) ) break;
block.push( lines[ i ] );

}

}

pending = parseDoc( block );
continue;

}

// class declaration
const cm = line.match( /^class\s+(\w+)(?:\s+extends\s+(\w+))?/ );
if ( cm ) {

curName = cm[ 1 ];
cur = THREE[ curName ] || ( THREE[ curName ] = {} );
// Declaring the class with a function `!type` makes Tern treat it as a
// constructor, so `new THREE.X()` resolves to X.prototype. The signature
// is refined once the constructor's JSDoc is read.
cur[ '!type' ] = 'fn()';
cur.prototype = cur.prototype || {};
if ( cm[ 2 ] && classes.has( cm[ 2 ] ) ) cur.prototype[ '!proto' ] = 'THREE.' + cm[ 2 ] + '.prototype';
if ( pending && pending.doc ) cur[ '!doc' ] = pending.doc;
pending = null;
continue;

}

// end of a top-level class
if ( line === '}' ) { cur = null; curName = null; pending = null; continue; }

if ( ! cur || ! pending ) { if ( t === '' ) continue; pending = null; continue; }

// inside a class, the line right after a JSDoc block:

// instance field: \t\tthis.name =
let m = line.match( /^\t\tthis\.(\w+)\s*=/ );
if ( m ) {

const ty = mapType( pending.atType, classes ) || '?';
cur.prototype[ m[ 1 ] ] = entry( ty, pending );
pending = null;
continue;

}

// static method: \tstatic name(
m = line.match( /^\tstatic\s+(\w+)\s*\(/ );
if ( m ) {

cur[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
pending = null;
continue;

}

// accessor: \tget name() / \tset name(
m = line.match( /^\t(?:get|set)\s+(\w+)\s*\(/ );
if ( m ) {

const ty = mapType( pending.atType || pending.returns, classes ) || '?';
if ( ! cur.prototype[ m[ 1 ] ] ) cur.prototype[ m[ 1 ] ] = entry( ty, pending );
pending = null;
continue;

}

// constructor: \tconstructor( -> refine the class signature
if ( /^\tconstructor\s*\(/.test( line ) ) {

cur[ '!type' ] = 'fn(' + pending.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' ) + ')';
pending = null;
continue;

}

// instance method: \t[async ]name(
m = line.match( /^\t(?:async\s+)?(\w+)\s*\(/ );
if ( m ) {

cur.prototype[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
pending = null;
continue;

}

pending = null;

}

return { '!name': 'threejs', THREE };

}
Loading
Loading