Skip to content

Conversation

@Mugen87
Copy link
Collaborator

@Mugen87 Mugen87 commented Mar 21, 2025

Related issue: #30748

Description

The PR is a first attempt to implement a client-side redirect from the old to the new documentation.

Since the old documentation is iFrame based, I've realized there is a single spot for implementing a redirect in index.html.

The build path of the new docs has been updated. The docs are not built into /docs_new/<package>/<version> anymore but just /docs_new. That makes the setup a bit simpler.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 24, 2025

@mrdoob For the next release r175, I recommend the following steps:

  • Publish the new docs via GitHub Pages so they are accessible via https://threejs.org/docs_new.
  • Update the links to the documentation at the homepage and GitHub to https://threejs.org/docs_new.
  • As discussed in Remove old docs. #30748, keep http://threejs.org/docs hosted and use this PR to redirect to the new doc pages.

@mrdoob
Copy link
Owner

mrdoob commented Mar 25, 2025

The build path of the new docs has been updated. The docs are not built into /docs_new/<package>/<version> anymore but just /docs_new. That makes the setup a bit simpler.

Great! Yeah, I was going to ask why did we need <package>/<version> in the url.

Just had a look a the new docs (finally)... They're looking pretty good!

However, I think they still need a few more tweaks (which I will do for r176).
Lets wait until r176 for the redirection code.

In the meantime...

Now that we have a jsdocs, can you investigate if we're able to generate a llms.txt too?

@Mugen87 Mugen87 added this to the r176 milestone Mar 25, 2025
@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

Now that we have a jsdocs, can you investigate if we're able to generate a llms.txt too?

When I understand the description of https://llmstxt.org/ correctly, the idea is to generate for each HTML documentation page a markdown version. So for AmbientLight.html there would be additionally AmbientLight.html.md and so on. The contents would be the same just formatted differently.

Yes, we can use JSDoc to generate such files but that requires a complete separate JSDoc template. The predefined tmpl template files of the default JSDoc theme can't be used since they output HTML. So all tmpl files must be migrated to Markdown. The default template also uses some JS helper functions (e.g. for link generation) that assume HTML output. These must be updated as well.

When thinking about this: If such a LLM JSDoc template is developed, it should be a separate project since any JSDoc documentation could use it to generate LLM friendly markdown files.

However, developing a new template is maybe not necessary since there is an existing project for generating markdown from JSDoc: https://github.com/jsdoc2md/jsdoc-to-markdown

I've tried to convert AudioListener to markdown with that tool (jsdoc2md src/audio/AudioListener.js) and below is the outcome. Unfortunately, it is not fully spec conform (at least the title must be h1). But this can be solved with a post-processing step or by configuring jsdoc-to-markdown (it seems to have some customization options). I wonder if someone from the llmstxt team could have a look at the below Markdown and recommend what to change.

<a name="AudioListener"></a>

## AudioListener ⇐ <code>Object3D</code>
The class represents a virtual listener of the all positional and non-positional audio effects
in the scene. A three.js application usually creates a single listener. It is a mandatory
constructor parameter for audios entities like [Audio](Audio) and [PositionalAudio](PositionalAudio).

In most cases, the listener object is a child of the camera. So the 3D transformation of the
camera represents the 3D transformation of the listener.

**Kind**: global class  
**Extends**: <code>Object3D</code>  

* [AudioListener](#AudioListener) ⇐ <code>Object3D</code>
    * [new AudioListener()](#new_AudioListener_new)
    * [.context](#AudioListener+context) : <code>AudioContext</code>
    * [.gain](#AudioListener+gain) : <code>GainNode</code>
    * [.filter](#AudioListener+filter) : <code>AudioNode</code>
    * [.timeDelta](#AudioListener+timeDelta) : <code>number</code>
    * [.getInput()](#AudioListener+getInput) ⇒ <code>GainNode</code>
    * [.removeFilter()](#AudioListener+removeFilter) ⇒ [<code>AudioListener</code>](#AudioListener)
    * [.getFilter()](#AudioListener+getFilter) ⇒ <code>AudioNode</code>
    * [.setFilter(value)](#AudioListener+setFilter) ⇒ [<code>AudioListener</code>](#AudioListener)
    * [.getMasterVolume()](#AudioListener+getMasterVolume) ⇒ <code>number</code>
    * [.setMasterVolume(value)](#AudioListener+setMasterVolume) ⇒ [<code>AudioListener</code>](#AudioListener)

<a name="new_AudioListener_new"></a>

### new AudioListener()
Constructs a new audio listener.

<a name="AudioListener+context"></a>

### audioListener.context : <code>AudioContext</code>
The native audio context.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Read only**: true  
<a name="AudioListener+gain"></a>

### audioListener.gain : <code>GainNode</code>
The gain node used for volume control.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Read only**: true  
<a name="AudioListener+filter"></a>

### audioListener.filter : <code>AudioNode</code>
An optional filter.

Defined via [setFilter](#AudioListener+setFilter).

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Default**: <code>null</code>  
**Read only**: true  
<a name="AudioListener+timeDelta"></a>

### audioListener.timeDelta : <code>number</code>
Time delta values required for `linearRampToValueAtTime()` usage.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Default**: <code>0</code>  
**Read only**: true  
<a name="AudioListener+getInput"></a>

### audioListener.getInput() ⇒ <code>GainNode</code>
Returns the listener's input node.

This method is used by other audio nodes to connect to this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>GainNode</code> - The input node.  
<a name="AudioListener+removeFilter"></a>

### audioListener.removeFilter() ⇒ [<code>AudioListener</code>](#AudioListener)
Removes the current filter from this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  
<a name="AudioListener+getFilter"></a>

### audioListener.getFilter() ⇒ <code>AudioNode</code>
Returns the current set filter.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>AudioNode</code> - The filter.  
<a name="AudioListener+setFilter"></a>

### audioListener.setFilter(value) ⇒ [<code>AudioListener</code>](#AudioListener)
Sets the given filter to this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  

| Param | Type | Description |
| --- | --- | --- |
| value | <code>AudioNode</code> | The filter to set. |

<a name="AudioListener+getMasterVolume"></a>

### audioListener.getMasterVolume() ⇒ <code>number</code>
Returns the applications master volume.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>number</code> - The master volume.  
<a name="AudioListener+setMasterVolume"></a>

### audioListener.setMasterVolume(value) ⇒ [<code>AudioListener</code>](#AudioListener)
Sets the applications master volume. This volume setting affects
all audio nodes in the scene.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  

| Param | Type | Description |
| --- | --- | --- |
| value | <code>number</code> | The master volume to set. |

@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2025

Ah, I was searching for "jsdoc to llms.txt" but "jsdoc to markdown" makes more sense.

It actually needs to be in a single file. The whole documentation with all the classes in a single llms.txt file.

People developing using cursor and windsurf can then add the file for the llm/agent:

Screenshot 2025-03-26 at 19 54 01 Screenshot 2025-03-26 at 19 54 22

And yes, this would definitely be a different thing from npm run build-docs.

@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2025

Thinking about the jsdoc template system...

Could we make it so jsdocs generate files like these?
https://github.com/mrdoob/three.js/blob/dev/docs/api/en/audio/AudioListener.html

That way we could just transparently replace the current documentation without having to do any redirects... 🤔

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

I've never seen a JSDoc-based documentation like that since the entire linking logic of JSDoc assumes all doc pages are located in the same directory. I recommend to use the JSDoc standard instead of building a custom solution.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

It actually needs to be in a single file. The whole documentation with all the classes in a single llms.txt file.

I'll try to figure out a script utilizing jsdoc2md that does that for us. 🙌

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 27, 2025

Below is the mentioned script and also the generated file llms.txt as a zip. Can you check if it is compatible with the mentioned tools?

llms.txt.zip

import fs from 'fs/promises';
import path from 'path';

import jsdoc2md from 'jsdoc-to-markdown';

// This script converts the API documentation from JSDoc to Markdown and writes everything
// into a single build/llms.txt output file.

build();

async function build() {

	// gather all files potentially holding JS Doc

	const files = [];

	const config = JSON.parse( await fs.readFile( 'utils/docs/jsdoc.config.json', 'utf8' ) );

	const includes = config.source.include;
	const excludes = config.source.exclude;

	for ( const include of includes ) {

		const includeFiles = await toArray( readAllFiles( include ) ); // Replace toArray() with Array.fromAsync() when supported

		// filter out excluded and non-js files

		files.push( ... includeFiles.filter( function ( file ) {

			for ( const exclude of excludes ) {

				if ( file.startsWith( exclude ) === true || file.endsWith( '.js' ) === false ) {

					return false;

				}

			}

			return true;

		} ) );

	}

	// create output directory if not existing

	const outputDir = 'utils/llm/build';
	if ( await fs.stat( outputDir ) === false ) await fs.mkdir( outputDir );

	// create output path and overwrite existing file

	const outputPath = path.join( outputDir, 'llms.txt' );
	await fs.writeFile( outputPath, '', { encoding: 'utf8', flag: 'w' } );

	// convert JSDoc to Markdown and save in output file

	await generateTitle( outputPath );
	await generateAPI( outputPath, files );

}

// helpers

async function generateTitle( outputPath ) {

	await fs.appendFile( outputPath, '# three.js \n\n' );
	await fs.appendFile( outputPath, '> JavaScript 3D Library. \n\n' );

}

async function generateAPI( outputPath, files ) {

	for ( const file of files ) {

		await fs.appendFile( outputPath, await jsdoc2md.render( { files: file } ) );

	}

}

async function* readAllFiles( dir ) {

	const files = await fs.readdir( dir, { withFileTypes: true } );

	for ( const file of files ) {

		if ( file.isDirectory() ) {

			yield* readAllFiles( path.join( dir, file.name ) );

		} else {

			yield path.join( dir, file.name );

		}

	}


}

async function toArray( asyncIterator ) {

	const arr = [];
	for await ( const i of asyncIterator ) arr.push( i );
	return arr;

}

If the script works out, I'll file a PR. It is supposed to be located in utils/llm.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 28, 2025

I split the changes of this PR into two.

@trenchfryer
Copy link

im going to try this.
hopefully makes unpacking API docs for AI easier

@mrdoob
Copy link
Owner

mrdoob commented Mar 28, 2025

@Mugen87

Below is the mentioned script and also the generated file llms.txt as a zip. Can you check if it is compatible with the mentioned tools?

llms.txt.zip

Thanks for looking into this 🙏

The size is a problem though, 3.7MB is more space than the AIs can handle. I'm trying to look for ways to work around this.

Someone on twitter also shared this npm module: https://www.npmjs.com/package/jsdoc-to-markdown

Edit: Svelte's approach seems interesting...
https://khromov.se/getting-better-ai-llm-assistance-for-svelte-5-and-sveltekit/

@mrdoob
Copy link
Owner

mrdoob commented Mar 28, 2025

@Mugen87

I've never seen a JSDoc-based documentation like that since the entire linking logic of JSDoc assumes all doc pages are located in the same directory. I recommend to use the JSDoc standard instead of building a custom solution.

Yeah, that sounds good to me.

However, after working on 49f25c6 today, I'm pretty sure we can keep the docs/ folder and add some javascript in docs/index.html that redirects hash-based urls to the new urls without users noticing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants