Просмотр исходного кода

New Docs: Generate old docs system and design (#32036)

* New Docs: Generate old doc system.

* New Docs: Added THREE instance and ASCII Art.

* New Docs: Templates clean up.

* New Docs: Clean up.

* New Docs: Avoid generating redundant tags.

* New Docs: Fixed search results and link colors.

* New Docs: Implemented full inheritance breadcrumbs.

* New Docs: Reimplemented old css.

* New Docs: Fixed hash links.

* New Docs: Implemented @marcofugaro search results design.

* New Docs: Clean up.

* New Docs: Clean up.

* New Docs: Do not include private properties/methods in results.

* New Docs: Separated Global from TSL.

* New Docs: Clean up.

* New Docs: Clean up.

* New Docs: Clean up.

* New Docs: Removed fuse.js.

* New Docs: Replaced examples link with manual.

* New Docs: Removed (nullable) from properties/methods.

* New Docs: Bring back geometry/materials/... viewers.

* New Docs: Separate code snippet from description.

* New Docs: Removed unused div container.

* New Docs: Moved Import section before Constructor section.

* New Docs: Ensure TSL is always uppercase.

* New Docs: Clean up.

* New Docs: Added missing @tsl tags.

* New Docs: Added missing @tsl and @private tags.

* New Docs: Simplified TSL handling.

* Added webgpu_materials_basic to puppeteer exceition list.
mrdoob 4 месяцев назад
Родитель
Сommit
a33da3ceec
56 измененных файлов с 3892 добавлено и 1087 удалено
  1. 2 0
      examples/jsm/tsl/display/DenoiseNode.js
  2. 1 0
      examples/jsm/tsl/display/boxBlur.js
  3. 1 0
      examples/jsm/tsl/display/hashBlur.js
  4. 20 0
      examples/jsm/tsl/lighting/TiledLightsNode.js
  5. 44 0
      files/main.css
  6. 1 0
      src/geometries/BoxGeometry.js
  7. 1 0
      src/geometries/CapsuleGeometry.js
  8. 1 0
      src/geometries/CircleGeometry.js
  9. 1 0
      src/geometries/ConeGeometry.js
  10. 1 0
      src/geometries/CylinderGeometry.js
  11. 1 0
      src/geometries/DodecahedronGeometry.js
  12. 1 0
      src/geometries/ExtrudeGeometry.js
  13. 1 0
      src/geometries/IcosahedronGeometry.js
  14. 1 0
      src/geometries/LatheGeometry.js
  15. 1 0
      src/geometries/OctahedronGeometry.js
  16. 1 0
      src/geometries/PlaneGeometry.js
  17. 1 0
      src/geometries/RingGeometry.js
  18. 1 0
      src/geometries/ShapeGeometry.js
  19. 1 0
      src/geometries/SphereGeometry.js
  20. 1 0
      src/geometries/TetrahedronGeometry.js
  21. 1 0
      src/geometries/TorusGeometry.js
  22. 1 0
      src/geometries/TorusKnotGeometry.js
  23. 1 0
      src/geometries/TubeGeometry.js
  24. 1 0
      src/materials/MeshBasicMaterial.js
  25. 1 0
      src/materials/MeshDepthMaterial.js
  26. 1 0
      src/materials/MeshLambertMaterial.js
  27. 1 0
      src/materials/MeshMatcapMaterial.js
  28. 1 0
      src/materials/MeshNormalMaterial.js
  29. 1 0
      src/materials/MeshPhongMaterial.js
  30. 1 0
      src/materials/MeshPhysicalMaterial.js
  31. 1 0
      src/materials/MeshStandardMaterial.js
  32. 1 0
      src/materials/MeshToonMaterial.js
  33. 1 0
      src/nodes/core/ArrayNode.js
  34. 15 0
      src/nodes/core/NodeUtils.js
  35. 25 0
      src/nodes/gpgpu/SubgroupFunctionNode.js
  36. 2 0
      src/nodes/lighting/ShadowFilterNode.js
  37. 2 0
      src/nodes/lighting/ShadowNode.js
  38. 1 0
      src/objects/SkinnedMesh.js
  39. 9 0
      src/renderers/common/RendererUtils.js
  40. 1 0
      test/e2e/puppeteer.js
  41. 194 29
      utils/docs/template/publish.js
  42. 681 0
      utils/docs/template/static/index.html
  43. 290 0
      utils/docs/template/static/scenes/bones-browser.html
  44. 285 0
      utils/docs/template/static/scenes/ccdiksolver-browser.html
  45. 806 0
      utils/docs/template/static/scenes/geometry-browser.html
  46. 818 0
      utils/docs/template/static/scenes/material-browser.html
  47. 0 8
      utils/docs/template/static/scripts/fuse/fuse.js
  48. 22 229
      utils/docs/template/static/scripts/page.js
  49. 0 159
      utils/docs/template/static/scripts/search.js
  50. 302 270
      utils/docs/template/static/styles/page.css
  51. 153 150
      utils/docs/template/tmpl/container.tmpl
  52. 38 41
      utils/docs/template/tmpl/details.tmpl
  53. 14 51
      utils/docs/template/tmpl/layout.tmpl
  54. 37 40
      utils/docs/template/tmpl/members.tmpl
  55. 71 76
      utils/docs/template/tmpl/method.tmpl
  56. 30 34
      utils/docs/template/tmpl/params.tmpl

+ 2 - 0
examples/jsm/tsl/display/DenoiseNode.js

@@ -259,6 +259,7 @@ export default DenoiseNode;
 /**
  * Generates denoise samples based on the given parameters.
  *
+ * @private
  * @param {number} numSamples - The number of samples.
  * @param {number} numRings - The number of rings.
  * @param {number} radiusExponent - The radius exponent.
@@ -283,6 +284,7 @@ function generateDenoiseSamples( numSamples, numRings, radiusExponent ) {
 /**
  * Generates a default noise texture for the given size.
  *
+ * @private
  * @param {number} [size=64] - The texture size.
  * @return {DataTexture} The generated noise texture.
  */

+ 1 - 0
examples/jsm/tsl/display/boxBlur.js

@@ -14,6 +14,7 @@ import { Fn, vec2, uv, Loop, vec4, premultiplyAlpha, unpremultiplyAlpha, max, in
  *
  * Reference: {@link https://github.com/lettier/3d-game-shaders-for-beginners/blob/master/demonstration/shaders/fragment/box-blur.frag}.
  *
+ * @tsl
  * @function
  * @param {Node<vec4>} textureNode - The texture node that should be blurred.
  * @param {Object} [options={}] - Additional options for the hash blur effect.

+ 1 - 0
examples/jsm/tsl/display/hashBlur.js

@@ -12,6 +12,7 @@ import { float, Fn, vec2, uv, sin, rand, degrees, cos, Loop, vec4, premultiplyAl
  *
  * Reference: {@link https://www.shadertoy.com/view/4lXXWn}.
  *
+ * @tsl
  * @function
  * @param {Node<vec4>} textureNode - The texture node that should be blurred.
  * @param {Node<float>} [bluramount=float(0.1)] - This node determines the amount of blur.

+ 20 - 0
examples/jsm/tsl/lighting/TiledLightsNode.js

@@ -5,6 +5,17 @@ import {
 	Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight
 } from 'three/tsl';
 
+/**
+ * TSL function that checks if a circle intersects with an axis-aligned bounding box (AABB).
+ *
+ * @tsl
+ * @function
+ * @param {Node<vec2>} circleCenter - The center of the circle.
+ * @param {Node<float>} radius - The radius of the circle.
+ * @param {Node<vec2>} minBounds - The minimum bounds of the AABB.
+ * @param {Node<vec2>} maxBounds - The maximum bounds of the AABB.
+ * @return {Node<bool>} True if the circle intersects the AABB.
+ */
 export const circleIntersectsAABB = /*@__PURE__*/ Fn( ( [ circleCenter, radius, minBounds, maxBounds ] ) => {
 
 	// Find the closest point on the AABB to the circle's center using method chaining
@@ -419,4 +430,13 @@ class TiledLightsNode extends LightsNode {
 
 export default TiledLightsNode;
 
+/**
+ * TSL function that creates a tiled lights node.
+ *
+ * @tsl
+ * @function
+ * @param {number} [maxLights=1024] - The maximum number of lights.
+ * @param {number} [tileSize=32] - The tile size.
+ * @return {TiledLightsNode} The tiled lights node.
+ */
 export const tiledLights = /*@__PURE__*/ nodeProxy( TiledLightsNode );

+ 44 - 0
files/main.css

@@ -304,6 +304,50 @@ hr {
 		padding: 0 var(--panel-padding) var(--panel-padding) var(--panel-padding);
 	}
 
+	#panel #searchResults {
+		flex: 1;
+		overflow-y: auto;
+		overflow-x: hidden;
+		-webkit-overflow-scrolling: touch;
+		padding: 0 var(--panel-padding) var(--panel-padding) var(--panel-padding);
+	}
+
+		#panel #searchResults .search-result-group {
+			margin-bottom: 12px;
+		}
+
+		#panel #searchResults .search-result-class {
+			display: block;
+			color: var(--text-color);
+			font-weight: 500;
+			margin-bottom: 2px;
+		}
+
+		#panel #searchResults .search-result-class:hover {
+			color: var(--color-blue);
+		}
+
+		#panel #searchResults .search-result-member {
+			display: block;
+			color: var(--text-color);
+			margin-left: 16px;
+			margin-bottom: 1px;
+		}
+
+		#panel #searchResults .search-result-member:hover {
+			color: var(--color-blue);
+		}
+
+		#panel #searchResults .search-result-class.selected,
+		#panel #searchResults .search-result-member.selected {
+			color: var(--color-blue);
+			text-decoration: underline;
+		}
+
+		#panel #searchResults strong {
+			font-weight: 600;
+		}
+
 		#panel #content ul {
 			list-style-type: none;
 			padding: 0px;

+ 1 - 0
src/geometries/BoxGeometry.js

@@ -15,6 +15,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#BoxGeometry
  */
 class BoxGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/CapsuleGeometry.js

@@ -13,6 +13,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#CapsuleGeometry
  */
 class CapsuleGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/CircleGeometry.js

@@ -19,6 +19,7 @@ import { Vector2 } from '../math/Vector2.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#CircleGeometry
  */
 class CircleGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/ConeGeometry.js

@@ -11,6 +11,7 @@ import { CylinderGeometry } from './CylinderGeometry.js';
  * ```
  *
  * @augments CylinderGeometry
+ * @demo scenes/geometry-browser.html#ConeGeometry
  */
 class ConeGeometry extends CylinderGeometry {
 

+ 1 - 0
src/geometries/CylinderGeometry.js

@@ -14,6 +14,7 @@ import { Vector2 } from '../math/Vector2.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#CylinderGeometry
  */
 class CylinderGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/DodecahedronGeometry.js

@@ -11,6 +11,7 @@ import { PolyhedronGeometry } from './PolyhedronGeometry.js';
  * ```
  *
  * @augments PolyhedronGeometry
+ * @demo scenes/geometry-browser.html#DodecahedronGeometry
  */
 class DodecahedronGeometry extends PolyhedronGeometry {
 

+ 1 - 0
src/geometries/ExtrudeGeometry.js

@@ -27,6 +27,7 @@ import { error } from '../utils.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#ExtrudeGeometry
  */
 class ExtrudeGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/IcosahedronGeometry.js

@@ -11,6 +11,7 @@ import { PolyhedronGeometry } from './PolyhedronGeometry.js';
  * ```
  *
  * @augments PolyhedronGeometry
+ * @demo scenes/geometry-browser.html#IcosahedronGeometry
  */
 class IcosahedronGeometry extends PolyhedronGeometry {
 

+ 1 - 0
src/geometries/LatheGeometry.js

@@ -19,6 +19,7 @@ import { clamp } from '../math/MathUtils.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#LatheGeometry
  */
 class LatheGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/OctahedronGeometry.js

@@ -11,6 +11,7 @@ import { PolyhedronGeometry } from './PolyhedronGeometry.js';
  * ```
  *
  * @augments PolyhedronGeometry
+ * @demo scenes/geometry-browser.html#OctahedronGeometry
  */
 class OctahedronGeometry extends PolyhedronGeometry {
 

+ 1 - 0
src/geometries/PlaneGeometry.js

@@ -12,6 +12,7 @@ import { Float32BufferAttribute } from '../core/BufferAttribute.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#PlaneGeometry
  */
 class PlaneGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/RingGeometry.js

@@ -14,6 +14,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#RingGeometry
  */
 class RingGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/ShapeGeometry.js

@@ -19,6 +19,7 @@ import { Vector2 } from '../math/Vector2.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#ShapeGeometry
  */
 class ShapeGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/SphereGeometry.js

@@ -13,6 +13,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#SphereGeometry
  */
 class SphereGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/TetrahedronGeometry.js

@@ -11,6 +11,7 @@ import { PolyhedronGeometry } from './PolyhedronGeometry.js';
  * ```
  *
  * @augments PolyhedronGeometry
+ * @demo scenes/geometry-browser.html#TetrahedronGeometry
  */
 class TetrahedronGeometry extends PolyhedronGeometry {
 

+ 1 - 0
src/geometries/TorusGeometry.js

@@ -13,6 +13,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#TorusGeometry
  */
 class TorusGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/TorusKnotGeometry.js

@@ -15,6 +15,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#TorusKnotGeometry
  */
 class TorusKnotGeometry extends BufferGeometry {
 

+ 1 - 0
src/geometries/TubeGeometry.js

@@ -29,6 +29,7 @@ import { Vector3 } from '../math/Vector3.js';
  * ```
  *
  * @augments BufferGeometry
+ * @demo scenes/geometry-browser.html#TubeGeometry
  */
 class TubeGeometry extends BufferGeometry {
 

+ 1 - 0
src/materials/MeshBasicMaterial.js

@@ -9,6 +9,7 @@ import { Euler } from '../math/Euler.js';
  * This material is not affected by lights.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshBasicMaterial
  */
 class MeshBasicMaterial extends Material {
 

+ 1 - 0
src/materials/MeshDepthMaterial.js

@@ -6,6 +6,7 @@ import { BasicDepthPacking } from '../constants.js';
  * near and far plane. White is nearest, black is farthest.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshDepthMaterial
  */
 class MeshDepthMaterial extends Material {
 

+ 1 - 0
src/materials/MeshLambertMaterial.js

@@ -19,6 +19,7 @@ import { Euler } from '../math/Euler.js';
  * {@link MeshPhysicalMaterial}, at the cost of some graphical accuracy.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshLambertMaterial
  */
 class MeshLambertMaterial extends Material {
 

+ 1 - 0
src/materials/MeshMatcapMaterial.js

@@ -13,6 +13,7 @@ import { Color } from '../math/Color.js';
  * shadows.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshMatcapMaterial
  */
 class MeshMatcapMaterial extends Material {
 

+ 1 - 0
src/materials/MeshNormalMaterial.js

@@ -6,6 +6,7 @@ import { Vector2 } from '../math/Vector2.js';
  * A material that maps the normal vectors to RGB colors.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshNormalMaterial
  */
 class MeshNormalMaterial extends Material {
 

+ 1 - 0
src/materials/MeshPhongMaterial.js

@@ -17,6 +17,7 @@ import { Euler } from '../math/Euler.js';
  * some graphical accuracy.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshPhongMaterial
  */
 class MeshPhongMaterial extends Material {
 

+ 1 - 0
src/materials/MeshPhysicalMaterial.js

@@ -27,6 +27,7 @@ import { clamp } from '../math/MathUtils.js';
  * best results, always specify an environment map when using this material.
  *
  * @augments MeshStandardMaterial
+ * @demo scenes/material-browser.html#MeshPhysicalMaterial
  */
 class MeshPhysicalMaterial extends MeshStandardMaterial {
 

+ 1 - 0
src/materials/MeshStandardMaterial.js

@@ -36,6 +36,7 @@ import { Euler } from '../math/Euler.js';
  * (pdf), by Brent Burley.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshStandardMaterial
  */
 class MeshStandardMaterial extends Material {
 

+ 1 - 0
src/materials/MeshToonMaterial.js

@@ -7,6 +7,7 @@ import { Color } from '../math/Color.js';
  * A material implementing toon shading.
  *
  * @augments Material
+ * @demo scenes/material-browser.html#MeshToonMaterial
  */
 class MeshToonMaterial extends Material {
 

+ 1 - 0
src/nodes/core/ArrayNode.js

@@ -11,6 +11,7 @@ import { addMethodChaining, nodeObject } from '../tsl/TSLCore.js';
  * ] );
  *
  * const redColor = tintColors.element( 0 );
+ * ```
  *
  * @augments TempNode
  */

+ 15 - 0
src/nodes/core/NodeUtils.js

@@ -50,6 +50,7 @@ function cyrb53( value, seed = 0 ) {
 /**
  * Computes a hash for the given string.
  *
+ * @private
  * @method
  * @param {string} str - The string to be hashed.
  * @return {number} The hash.
@@ -59,6 +60,7 @@ export const hashString = ( str ) => cyrb53( str );
 /**
  * Computes a hash for the given array.
  *
+ * @private
  * @method
  * @param {Array<number>} array - The array to be hashed.
  * @return {number} The hash.
@@ -68,6 +70,7 @@ export const hashArray = ( array ) => cyrb53( array );
 /**
  * Computes a hash for the given list of parameters.
  *
+ * @private
  * @method
  * @param {...number} params - A list of parameters.
  * @return {number} The hash.
@@ -77,6 +80,7 @@ export const hash = ( ...params ) => cyrb53( params );
 /**
  * Computes a cache key for the given node.
  *
+ * @private
  * @method
  * @param {Object|Node} object - The object to be hashed.
  * @param {boolean} [force=false] - Whether to force a cache key computation or not.
@@ -106,6 +110,7 @@ export function getCacheKey( object, force = false ) {
  * This generator function can be used to iterate over the node children
  * of the given object.
  *
+ * @private
  * @generator
  * @param {Object} node - The object to be hashed.
  * @param {boolean} [toJSON=false] - Whether to return JSON or not.
@@ -175,6 +180,7 @@ const dataFromObject = /*@__PURE__*/ new WeakMap();
 /**
  * Returns the data type for the given the length.
  *
+ * @private
  * @method
  * @param {number} length - The length.
  * @return {string} The data type.
@@ -188,6 +194,7 @@ export function getTypeFromLength( length ) {
 /**
  * Returns the typed array for the given data type.
  *
+ * @private
  * @method
  * @param {string} type - The data type.
  * @return {TypedArray} The typed array.
@@ -221,6 +228,7 @@ export function getTypedArrayFromType( type ) {
 /**
  * Returns the length for the given data type.
  *
+ * @private
  * @method
  * @param {string} type - The data type.
  * @return {number} The length.
@@ -242,6 +250,7 @@ export function getLengthFromType( type ) {
 /**
  * Returns the gpu memory length for the given data type.
  *
+ * @private
  * @method
  * @param {string} type - The data type.
  * @return {number} The length.
@@ -263,6 +272,7 @@ export function getMemoryLengthFromType( type ) {
 /**
  * Returns the byte boundary for the given data type.
  *
+ * @private
  * @method
  * @param {string} type - The data type.
  * @return {number} The byte boundary.
@@ -284,6 +294,7 @@ export function getByteBoundaryFromType( type ) {
 /**
  * Returns the data type for the given value.
  *
+ * @private
  * @method
  * @param {any} value - The value.
  * @return {?string} The data type.
@@ -355,6 +366,7 @@ export function getValueType( value ) {
 /**
  * Returns the value/object for the given data type and parameters.
  *
+ * @private
  * @method
  * @param {string} type - The given type.
  * @param {...any} params - A parameter list.
@@ -425,6 +437,7 @@ export function getValueFromType( type, ...params ) {
 /**
  * Gets the object data that can be shared between different rendering steps.
  *
+ * @private
  * @param {Object} object - The object to get the data for.
  * @return {Object} The object data.
  */
@@ -446,6 +459,7 @@ export function getDataFromObject( object ) {
 /**
  * Converts the given array buffer to a Base64 string.
  *
+ * @private
  * @method
  * @param {ArrayBuffer} arrayBuffer - The array buffer.
  * @return {string} The Base64 string.
@@ -469,6 +483,7 @@ export function arrayBufferToBase64( arrayBuffer ) {
 /**
  * Converts the given Base64 string to an array buffer.
  *
+ * @private
  * @method
  * @param {string} base64 - The Base64 string.
  * @return {ArrayBuffer} The array buffer.

+ 25 - 0
src/nodes/gpgpu/SubgroupFunctionNode.js

@@ -203,6 +203,7 @@ export default SubgroupFunctionNode;
  * Returns true if this invocation has the lowest subgroup_invocation_id
  * among active invocations in the subgroup.
  *
+ * @tsl
  * @method
  * @return {bool} The result of the computation.
  */
@@ -212,6 +213,7 @@ export const subgroupElect = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode
  * Returns a set of bitfields where the bit corresponding to subgroup_invocation_id
  * is 1 if pred is true for that active invocation and 0 otherwise.
  *
+ * @tsl
  * @method
  * @param {bool} pred - A boolean that sets the bit corresponding to the invocations subgroup invocation id.
  * @return {vec4<u32>}- A bitfield corresponding to the pred value of each subgroup invocation.
@@ -221,6 +223,7 @@ export const subgroupBallot = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNod
 /**
  * A reduction that adds e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The accumulated result of the reduction operation.
@@ -230,6 +233,7 @@ export const subgroupAdd = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * An inclusive scan returning the sum of e for all active invocations with subgroup_invocation_id less than or equal to this invocation.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the inclusive scan by the current invocation.
  * @return {number} The accumulated result of the inclusive scan operation.
@@ -239,6 +243,7 @@ export const subgroupInclusiveAdd = /*@__PURE__*/ nodeProxyIntent( SubgroupFunct
 /**
  * An exclusive scan that returns the sum of e for all active invocations with subgroup_invocation_id less than this invocation.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the exclusive scan by the current invocation.
  * @return {number} The accumulated result of the exclusive scan operation.
@@ -248,6 +253,7 @@ export const subgroupExclusiveAdd = /*@__PURE__*/ nodeProxyIntent( SubgroupFunct
 /**
  * A reduction that multiplies e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The accumulated result of the reduction operation.
@@ -257,6 +263,7 @@ export const subgroupMul = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * An inclusive scan returning the product of e for all active invocations with subgroup_invocation_id less than or equal to this invocation.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the inclusive scan by the current invocation.
  * @return {number} The accumulated result of the inclusive scan operation.
@@ -266,6 +273,7 @@ export const subgroupInclusiveMul = /*@__PURE__*/ nodeProxyIntent( SubgroupFunct
 /**
  * An exclusive scan that returns the product of e for all active invocations with subgroup_invocation_id less than this invocation.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the exclusive scan by the current invocation.
  * @return {number} The accumulated result of the exclusive scan operation.
@@ -275,6 +283,7 @@ export const subgroupExclusiveMul = /*@__PURE__*/ nodeProxyIntent( SubgroupFunct
 /**
  * A reduction that performs a bitwise and of e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The result of the reduction operation.
@@ -284,6 +293,7 @@ export const subgroupAnd = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * A reduction that performs a bitwise or of e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The result of the reduction operation.
@@ -293,6 +303,7 @@ export const subgroupOr = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode, S
 /**
  * A reduction that performs a bitwise xor of e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The result of the reduction operation.
@@ -302,6 +313,7 @@ export const subgroupXor = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * A reduction that performs a min of e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The result of the reduction operation.
@@ -311,6 +323,7 @@ export const subgroupMin = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * A reduction that performs a max of e among all active invocations and returns that result.
  *
+ * @tsl
  * @method
  * @param {number} e - The value provided to the reduction by the current invocation.
  * @return {number} The result of the reduction operation.
@@ -320,6 +333,7 @@ export const subgroupMax = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * Returns true if e is true for all active invocations in the subgroup.
  *
+ * @tsl
  * @method
  * @return {bool} The result of the computation.
  */
@@ -328,6 +342,7 @@ export const subgroupAll = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * Returns true if e is true for any active invocation in the subgroup
  *
+ * @tsl
  * @method
  * @return {bool} The result of the computation.
  */
@@ -336,6 +351,7 @@ export const subgroupAny = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode,
 /**
  * Broadcasts e from the active invocation with the lowest subgroup_invocation_id in the subgroup to all other active invocations.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to broadcast from the lowest subgroup invocation.
  * @param {number} id - The subgroup invocation to broadcast from.
@@ -346,6 +362,7 @@ export const subgroupBroadcastFirst = /*@__PURE__*/ nodeProxyIntent( SubgroupFun
 /**
  * Swaps e between invocations in the quad in the X direction.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to swap from the current invocation.
  * @return {number} The value received from the swap operation.
@@ -355,6 +372,7 @@ export const quadSwapX = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode, Su
 /**
  * Swaps e between invocations in the quad in the Y direction.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to swap from the current invocation.
  * @return {number} The value received from the swap operation.
@@ -364,6 +382,7 @@ export const quadSwapY = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNode, Su
 /**
  * Swaps e between invocations in the quad diagonally.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to swap from the current invocation.
  * @return {number} The value received from the swap operation.
@@ -373,6 +392,7 @@ export const quadSwapDiagonal = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionN
 /**
  * Broadcasts e from the invocation whose subgroup_invocation_id matches id, to all active invocations.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to broadcast from subgroup invocation 'id'.
  * @param {number} id - The subgroup invocation to broadcast from.
@@ -383,6 +403,7 @@ export const subgroupBroadcast = /*@__PURE__*/ nodeProxyIntent( SubgroupFunction
 /**
  * Returns v from the active invocation whose subgroup_invocation_id matches id
  *
+ * @tsl
  * @method
  * @param {number} v - The value to return from subgroup invocation id^mask.
  * @param {number} id - The subgroup invocation which returns the value v.
@@ -393,6 +414,7 @@ export const subgroupShuffle = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctionNo
 /**
  * Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id ^ mask.
  *
+ * @tsl
  * @method
  * @param {number} v - The value to return from subgroup invocation id^mask.
  * @param {number} mask - A bitmask that determines the target invocation via a XOR operation.
@@ -403,6 +425,7 @@ export const subgroupShuffleXor = /*@__PURE__*/ nodeProxyIntent( SubgroupFunctio
 /**
  * Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id - delta
  *
+ * @tsl
  * @method
  * @param {number} v - The value to return from subgroup invocation id^mask.
  * @param {number} delta - A value that offsets the current in.
@@ -413,6 +436,7 @@ export const subgroupShuffleUp = /*@__PURE__*/ nodeProxyIntent( SubgroupFunction
 /**
  * Returns v from the active invocation whose subgroup_invocation_id matches subgroup_invocation_id + delta
  *
+ * @tsl
  * @method
  * @param {number} v - The value to return from subgroup invocation id^mask.
  * @param {number} delta - A value that offsets the current subgroup invocation.
@@ -423,6 +447,7 @@ export const subgroupShuffleDown = /*@__PURE__*/ nodeProxyIntent( SubgroupFuncti
 /**
  * Broadcasts e from the quad invocation with id equal to id.
  *
+ * @tsl
  * @method
  * @param {number} e - The value to broadcast.
  * @return {number} The broadcast value.

+ 2 - 0
src/nodes/lighting/ShadowFilterNode.js

@@ -245,6 +245,8 @@ const linearShadowDistance = ( light ) => {
  * If not, it creates a new `NodeMaterial` configured for shadow rendering and stores it
  * in the `shadowMaterialLib` for future use.
  *
+ * @tsl
+ * @function
  * @param {Light} light - The light source for which the shadow material is needed.
  *                         If the light is a point light, a depth node is calculated
  *                         using the linear shadow distance.

+ 2 - 0
src/nodes/lighting/ShadowNode.js

@@ -30,6 +30,8 @@ const _shadowRenderObjectKeys = [];
 /**
  * Creates a function to render shadow objects in a scene.
  *
+ * @tsl
+ * @function
  * @param {Renderer} renderer - The renderer.
  * @param {LightShadow} shadow - The light shadow object containing shadow properties.
  * @param {number} shadowType - The type of shadow map (e.g., BasicShadowMap).

+ 1 - 0
src/objects/SkinnedMesh.js

@@ -33,6 +33,7 @@ const _ray = /*@__PURE__*/ new Ray();
  * or {@link FBXLoader } import respective models.
  *
  * @augments Mesh
+ * @demo scenes/bones-browser.html
  */
 class SkinnedMesh extends Mesh {
 

+ 9 - 0
src/renderers/common/RendererUtils.js

@@ -5,6 +5,7 @@ import { Color } from '../../math/Color.js';
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Object} [state={}] - The state.
@@ -36,6 +37,7 @@ export function saveRendererState( renderer, state = {} ) {
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Object} [state={}] - The state.
@@ -57,6 +59,7 @@ export function resetRendererState( renderer, state ) {
 /**
  * Restores the state of the given renderer from the given state object.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Object} state - The state to restore.
@@ -81,6 +84,7 @@ export function restoreRendererState( renderer, state ) {
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Scene} scene - The scene.
  * @param {Object} [state={}] - The state.
@@ -102,6 +106,7 @@ export function saveSceneState( scene, state = {} ) {
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Scene} scene - The scene.
  * @param {Object} [state={}] - The state.
@@ -122,6 +127,7 @@ export function resetSceneState( scene, state ) {
 /**
  * Restores the state of the given scene from the given state object.
  *
+ * @private
  * @function
  * @param {Scene} scene - The scene.
  * @param {Object} state - The state to restore.
@@ -139,6 +145,7 @@ export function restoreSceneState( scene, state ) {
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Scene} scene - The scene.
@@ -160,6 +167,7 @@ export function saveRendererAndSceneState( renderer, scene, state = {} ) {
  *
  * If not state object is provided, the function creates one.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Scene} scene - The scene.
@@ -178,6 +186,7 @@ export function resetRendererAndSceneState( renderer, scene, state ) {
 /**
  * Restores the state of the given renderer and scene from the given state object.
  *
+ * @private
  * @function
  * @param {Renderer} renderer - The renderer.
  * @param {Scene} scene - The scene.

+ 1 - 0
test/e2e/puppeteer.js

@@ -152,6 +152,7 @@ const exceptionList = [
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_lightprobe_cubecamera',
 	'webgpu_loader_materialx',
+	'webgpu_materials_basic',
 	'webgpu_materials_video',
 	'webgpu_materialx_noise',
 	'webgpu_morphtargets_face',

+ 194 - 29
utils/docs/template/publish.js

@@ -17,6 +17,7 @@ let view;
 
 const outdir = path.normalize( env.opts.destination );
 const themeOpts = ( env.opts.themeOpts ) || {};
+const categoryMap = {}; // Maps class names to their categories (Core, Addons, TSL)
 
 function mkdirSync( filepath ) {
 
@@ -143,23 +144,84 @@ function buildItemTypeStrings( item ) {
 
 function buildSearchListForData() {
 
-	const searchList = [];
+	const categories = {
+		'Core': [],
+		'Addons': [],
+		'Global': [],
+		'TSL': []
+	};
 
 	data().each( ( item ) => {
 
 		if ( item.kind !== 'package' && item.kind !== 'typedef' && ! item.inherited ) {
 
-			searchList.push( {
-				title: item.longname,
-				link: linkto( item.longname, item.name ),
-				description: item.description,
-			} );
+			// Extract the class name from the longname (e.g., "Animation#getAnimationLoop" -> "Animation")
+			const parts = item.longname.split( /[#~]/ );
+			const className = parts[ 0 ];
+
+			// If this item is a member/method of a class, check if the parent class exists
+			if ( parts.length > 1 ) {
+
+				// Find the parent class/module
+				const parentClass = find( { longname: className, kind: [ 'class', 'module' ] } );
+
+				// Only include if parent exists and is not private
+				if ( parentClass && parentClass.length > 0 && parentClass[ 0 ].access !== 'private' ) {
+
+					const category = categoryMap[ className ];
+					const entry = {
+						title: item.longname,
+						kind: item.kind
+					};
+
+					if ( category ) {
+
+						categories[ category ].push( entry );
+
+					}
+
+				}
+
+			} else {
+
+				// This is a top-level class/module/function - include if not private
+				if ( item.access !== 'private' ) {
+
+					let category = categoryMap[ className ];
+
+					// If not in categoryMap, determine category from @tsl tag
+					if ( ! category ) {
+
+						const hasTslTag = Array.isArray( item.tags ) && item.tags.some( tag => tag.title === 'tsl' );
+
+						if ( hasTslTag ) {
+
+							category = 'TSL';
+
+						} else {
+
+							category = 'Global';
+
+						}
+
+					}
+
+					const entry = {
+						title: item.longname,
+						kind: item.kind
+					};
+
+					categories[ category ].push( entry );
+
+				}
+
+			}
 
 		}
 
 	} );
 
-	return searchList;
+	return categories;
 
 }
 
@@ -194,8 +256,9 @@ function addNonParamAttributes( items ) {
 function addSignatureParams( f ) {
 
 	const params = f.params ? addParamAttributes( f.params ) : [];
+	const paramsString = params.join( ', ' );
 
-	f.signature = util.format( '%s( %s )', ( f.signature || '' ), params.join( ', ' ) );
+	f.signature = util.format( '%s(%s)', ( f.signature || '' ), paramsString ? ' ' + paramsString + ' ' : '' );
 
 }
 
@@ -217,7 +280,7 @@ function addSignatureReturns( f ) {
 
 	}
 
-	f.signature = `<span class="signature">${f.signature || ''}</span><span class="type-signature">${returnTypesString}</span>`;
+	f.signature = `<span class="signature">${f.signature || ''}</span>${returnTypesString ? `<span class="type-signature">${returnTypesString}</span>` : ''}`;
 
 }
 
@@ -225,16 +288,16 @@ function addSignatureTypes( f ) {
 
 	const types = f.type ? buildItemTypeStrings( f ) : [];
 
-	f.signature = `${f.signature || ''}<span class="type-signature">${types.length ? ` : ${types.join( ' | ' )}` : ''}</span>`;
+	f.signature = `${f.signature || ''}${types.length ? `<span class="type-signature"> : ${types.join( ' | ' )}</span>` : ''}`;
 
 }
 
 function addAttribs( f ) {
 
-	const attribs = helper.getAttribs( f ).filter( attrib => attrib !== 'static' );
+	const attribs = helper.getAttribs( f ).filter( attrib => attrib !== 'static' && attrib !== 'nullable' );
 	const attribsString = buildAttribsString( attribs );
 
-	f.attribs = util.format( '<span class="type-signature">%s</span>', attribsString );
+	f.attribs = attribsString ? util.format( '<span class="type-signature">%s</span>', attribsString ) : '';
 
 }
 
@@ -266,6 +329,34 @@ function getPathFromDoclet( { meta } ) {
 
 }
 
+function getFullAugmentsChain( doclet ) {
+
+	const chain = [];
+
+	if ( ! doclet || ! doclet.augments || ! doclet.augments.length ) {
+
+		return chain;
+
+	}
+
+	// Start with the immediate parent
+	const parentName = doclet.augments[0];
+	chain.push( parentName );
+
+	// Recursively find the parent's ancestors
+	const parentDoclet = find( { longname: parentName } );
+
+	if ( parentDoclet && parentDoclet.length > 0 ) {
+
+		const parentChain = getFullAugmentsChain( parentDoclet[0] );
+		chain.unshift( ...parentChain );
+
+	}
+
+	return chain;
+
+}
+
 function generate( title, docs, filename, resolveLinks ) {
 
 	let html;
@@ -276,10 +367,13 @@ function generate( title, docs, filename, resolveLinks ) {
 		env: env,
 		title: title,
 		docs: docs,
-		augments: docs && docs[0] ? docs[0].augments : null
+		augments: docs && docs[0] ? getFullAugmentsChain( docs[0] ) : null
 	};
 
-	const outpath = path.join( outdir, filename );
+	// Put HTML files in pages/ subdirectory
+	const pagesDir = path.join( outdir, 'pages' );
+	mkdirSync( pagesDir );
+	const outpath = path.join( pagesDir, filename );
 	html = view.render( 'container.tmpl', docData );
 
 	if ( resolveLinks ) {
@@ -366,12 +460,14 @@ function buildMainNav( items, itemsSeen, linktoFn ) {
 					const subCategory = path.split( '/' )[ 1 ];
 
 					pushNavItem( hierarchy, 'Core', subCategory, itemNav );
+					categoryMap[ item.longname ] = 'Core';
 
 				} else if ( path.startsWith( addonsDirectory ) ) {
 
 					const subCategory = path.split( '/' )[ 2 ];
 
 					pushNavItem( hierarchy, 'Addons', subCategory, itemNav );
+					categoryMap[ item.longname ] = 'Addons';
 
 				}
 
@@ -424,11 +520,11 @@ function buildGlobalsNav( globals, seen ) {
 
 		globals.forEach( ( { kind, longname, name, tags } ) => {
 
-			if ( kind !== 'typedef' && ! hasOwnProp.call( seen, longname ) && Array.isArray( tags ) ) {
+			if ( kind !== 'typedef' && ! hasOwnProp.call( seen, longname ) ) {
 
-				const tslTag = tags.find( tag => tag.title === 'tsl' );
+				const hasTslTag = Array.isArray( tags ) && tags.some( tag => tag.title === 'tsl' );
 
-				if ( tslTag !== undefined ) {
+				if ( hasTslTag ) {
 
 					tslNav += `<li>${linkto( longname, name )}</li>\n`;
 
@@ -477,7 +573,16 @@ function buildGlobalsNav( globals, seen ) {
 
 function pushNavItem( hierarchy, mainCategory, subCategory, itemNav ) {
 
-	subCategory = subCategory[ 0 ].toUpperCase() + subCategory.slice( 1 ); // capitalize
+	// Special case for TSL - keep it all uppercase
+	if ( subCategory.toLowerCase() === 'tsl' ) {
+
+		subCategory = 'TSL';
+
+	} else {
+
+		subCategory = subCategory[ 0 ].toUpperCase() + subCategory.slice( 1 ); // capitalize
+
+	}
 
 	if ( hierarchy.get( mainCategory ).get( subCategory ) === undefined ) {
 
@@ -703,7 +808,7 @@ exports.publish = ( taffyData, opts, tutorials ) => {
 
 	} );
 
-	// prepare import statements
+	// prepare import statements, demo tags, and extract code examples
 	data().each( doclet => {
 
 		if ( doclet.kind === 'class' || doclet.kind === 'module' ) {
@@ -715,6 +820,25 @@ exports.publish = ( taffyData, opts, tutorials ) => {
 				const importTag = tags.find( tag => tag.title === 'three_import' );
 				doclet.import = ( importTag !== undefined ) ? importTag.text : null;
 
+				const demoTag = tags.find( tag => tag.title === 'demo' );
+				doclet.demo = ( demoTag !== undefined ) ? demoTag.text : null;
+
+			}
+
+			// Extract code example from classdesc
+			if ( doclet.classdesc ) {
+
+				const codeBlockRegex = /<pre class="prettyprint source[^"]*"><code>([\s\S]*?)<\/code><\/pre>/;
+				const match = doclet.classdesc.match( codeBlockRegex );
+
+				if ( match ) {
+
+					doclet.codeExample = match[ 0 ];
+					// Remove the code example from classdesc
+					doclet.classdesc = doclet.classdesc.replace( codeBlockRegex, '' ).trim();
+
+				}
+
 			}
 
 		}
@@ -747,7 +871,43 @@ exports.publish = ( taffyData, opts, tutorials ) => {
 
 	if ( members.globals.length ) {
 
-		generate( 'Global', [ { kind: 'globalobj' } ], globalUrl );
+		// Split globals into TSL and non-TSL
+		const tslGlobals = [];
+		const nonTslGlobals = [];
+		const originalGlobals = members.globals;
+
+		originalGlobals.forEach( item => {
+
+			const hasTslTag = Array.isArray( item.tags ) && item.tags.some( tag => tag.title === 'tsl' );
+
+			if ( hasTslTag ) {
+
+				tslGlobals.push( item );
+
+				// Register each TSL item to link to TSL.html
+				helper.registerLink( item.longname, 'TSL.html#' + item.name );
+
+			} else {
+
+				nonTslGlobals.push( item );
+
+			}
+
+		} );
+
+		// Generate TSL.html for TSL functions
+		if ( tslGlobals.length ) {
+
+			generate( 'TSL', [ { kind: 'globalobj', isTSL: true } ], 'TSL.html' );
+
+		}
+
+		// Generate global.html for remaining globals
+		if ( nonTslGlobals.length ) {
+
+			generate( 'Global', [ { kind: 'globalobj' } ], globalUrl );
+
+		}
 
 	}
 
@@ -787,10 +947,19 @@ exports.publish = ( taffyData, opts, tutorials ) => {
 
 	} );
 
-	// Write navigation to separate file
+	// Build navigation HTML
+	const navHtml = buildNav( members );
+
+	// Generate index.html with embedded navigation
+	const indexTemplatePath = path.join( templatePath, 'static', 'index.html' );
+	let indexHtml = fs.readFileSync( indexTemplatePath, 'utf8' );
+
+	// Replace placeholder with actual navigation
+	indexHtml = indexHtml.replace( '<!--NAV_PLACEHOLDER-->', navHtml );
+
 	fs.writeFileSync(
-		path.join( outdir, 'nav.html' ),
-		buildNav( members ),
+		path.join( outdir, 'index.html' ),
+		indexHtml,
 		'utf8'
 	);
 
@@ -798,13 +967,9 @@ exports.publish = ( taffyData, opts, tutorials ) => {
 
 	const searchList = buildSearchListForData();
 
-	mkdirSync( path.join( outdir, 'data' ) );
-
 	fs.writeFileSync(
-		path.join( outdir, 'data', 'search.json' ),
-		JSON.stringify( {
-			list: searchList,
-		} )
+		path.join( outdir, 'search.json' ),
+		JSON.stringify( searchList, null, '\t' )
 	);
 
 };

+ 681 - 0
utils/docs/template/static/index.html

@@ -0,0 +1,681 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>three.js docs</title>
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)"/>
+		<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)" />
+		<link rel="stylesheet" type="text/css" href="/files/main.css">
+		<link type="text/css" rel="stylesheet" href="styles/page.css">
+		<!-- console sandbox -->
+		<script type="module">
+			import * as THREE from '../build/three.module.js';
+			window.THREE = THREE;
+		</script>
+	</head>
+	<body>
+		<div id="panel">
+
+			<div id="header">
+				<h1><a href="https://threejs.org">three.js</a></h1>
+
+				<div id="sections">
+					<span class="selected">docs</span>
+					<a href="../manual/">manual</a>
+				</div>
+
+				<div id="expandButton"></div>
+			</div>
+
+			<div id="panelScrim"></div>
+
+			<div id="contentWrapper">
+				<div id="inputWrapper">
+					<input placeholder="" type="text" id="filterInput" autocorrect="off" autocapitalize="off" spellcheck="false" />
+					<div id="clearSearchButton"></div>
+				</div>
+				<div id="searchResults" style="display: none;"></div>
+				<div id="content"><!--NAV_PLACEHOLDER--></div>
+			</div>
+
+		</div>
+
+		<iframe id="viewer" name="viewer"></iframe>
+
+		<script>
+
+		// Handle legacy URLs from old documentation structure
+		( function handleLegacyURLs() {
+
+			const hash = window.location.hash;
+
+			if ( hash.startsWith( '#api/' ) || hash.startsWith( '#examples/' ) ) {
+
+				const mappings = {
+
+					'3DMLoader': 'Rhino3dmLoader',
+
+					'BufferGeometryUtils': 'module-BufferGeometryUtils',
+					'CameraUtils': 'module-CameraUtils',
+					'SceneUtils': 'module-SceneUtils',
+					'SkeletonUtils': 'module-SkeletonUtils',
+					'UniformsUtils': 'module-UniformsUtils',
+
+					'DefaultLoadingManager': 'LoadingManager',
+					'Interpolations': 'module-Interpolations',
+
+					'Animation': 'global',
+					'BufferAttributeUsage': 'global',
+					'Core': 'global',
+					'CustomBlendingEquations': 'global',
+					'Materials': 'global',
+					'Textures': 'global'
+				};
+
+				const parts = hash.split( '/' );
+				let className = parts[ parts.length - 1 ];
+
+				if ( className ) {
+
+					if ( className in mappings ) className = mappings[ className ];
+
+					window.location.hash = className;
+
+				}
+
+			}
+
+		} )();
+
+		const panel = document.getElementById( 'panel' );
+		const content = document.getElementById( 'content' );
+		const expandButton = document.getElementById( 'expandButton' );
+		const clearSearchButton = document.getElementById( 'clearSearchButton' );
+		const panelScrim = document.getElementById( 'panelScrim' );
+		const filterInput = document.getElementById( 'filterInput' );
+		let iframe = document.getElementById( 'viewer' );
+
+		const pageLinks = {};
+		let navigation;
+		let isUserClick = false;
+		let searchData;
+
+		fetch( 'search.json' )
+			.then( response => response.json() )
+			.then( data => {
+
+				searchData = data;
+
+				if ( filterInput.value !== '' ) {
+
+					updateFilter();
+
+				}
+
+			} )
+			.catch( err => console.error( 'Failed to load search data:', err ) );
+
+		init();
+
+		function init() {
+
+			expandButton.onclick = function ( event ) {
+
+				event.preventDefault();
+				panel.classList.toggle( 'open' );
+
+			};
+
+			panelScrim.onclick = function ( event ) {
+
+				event.preventDefault();
+				panel.classList.toggle( 'open' );
+
+			};
+
+			filterInput.onfocus = function () {
+
+				panel.classList.add( 'searchFocused' );
+
+			};
+
+			filterInput.onblur = function () {
+
+				if ( filterInput.value === '' ) {
+
+					panel.classList.remove( 'searchFocused' );
+
+				}
+
+			};
+
+			filterInput.oninput = function () {
+
+				updateFilter();
+
+			};
+
+			clearSearchButton.onclick = function () {
+
+				filterInput.value = '';
+				updateFilter();
+				filterInput.focus();
+
+			};
+
+			window.onpopstate = createNewIframe;
+
+			setupNavigation();
+			createNewIframe();
+
+			filterInput.value = extractQuery();
+
+			if ( filterInput.value !== '' ) {
+
+				panel.classList.add( 'searchFocused' );
+
+			}
+
+		}
+
+		// Navigation Panel
+
+		function setupNavigation() {
+
+			navigation = content;
+
+			const selectedPage = window.location.hash.substring( 1 );
+
+			const links = navigation.querySelectorAll( 'a' );
+
+			links.forEach( link => {
+
+				const href = link.getAttribute( 'href' );
+
+				if ( href && href.includes( '.html' ) ) {
+
+					const match = href.match( /^([^#]+\.html)(#.*)?$/ );
+					if ( ! match ) return;
+
+					const htmlFile = match[ 1 ];
+					const anchor = match[ 2 ] || '';
+					const pageName = htmlFile.replace( /\.html$/, '' );
+					const fullPageName = pageName + anchor.replace( '#', '.' );
+					const pageURL = 'pages/' + htmlFile;
+
+					link.setAttribute( 'href', pageURL + anchor );
+					link.setAttribute( 'target', 'viewer' );
+					link.addEventListener( 'click', function ( event ) {
+
+						if ( event.button !== 0 || event.ctrlKey || event.altKey || event.metaKey ) return;
+
+						event.preventDefault();
+						isUserClick = true;
+						window.location.hash = fullPageName;
+						panel.classList.remove( 'open' );
+
+						navigation.querySelectorAll( 'a' ).forEach( function ( item ) {
+
+							item.classList.remove( 'selected' );
+
+						} );
+
+						link.classList.add( 'selected' );
+
+					} );
+
+					pageLinks[ fullPageName ] = {
+						linkElement: link,
+						pageURL: pageURL,
+						anchor: anchor,
+						href: href
+					};
+
+					if ( ! pageLinks[ pageName ] ) {
+
+						pageLinks[ pageName ] = {
+							linkElement: link,
+							pageURL: pageURL,
+							anchor: '',
+							href: htmlFile
+						};
+
+					}
+
+					if ( fullPageName === selectedPage || pageName === selectedPage ) {
+
+						link.classList.add( 'selected' );
+						link.scrollIntoView( { block: 'center' } );
+
+					}
+
+				}
+
+			} );
+
+		}
+
+		function extractQuery() {
+
+			const search = window.location.search;
+
+			if ( search.indexOf( '?q=' ) !== - 1 ) {
+
+				return decodeURI( search.slice( 3 ) );
+
+			}
+
+			return '';
+
+		}
+
+		function escapeRegExp( string ) {
+
+			string = string.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
+			return '(?=.*' + string.split( ' ' ).join( ')(?=.*' ) + ')';
+
+		}
+
+		function updateFilter() {
+
+			let v = filterInput.value.trim();
+			v = v.replace( /\s+/gi, ' ' );
+
+			const searchResults = document.getElementById( 'searchResults' );
+			const content = document.getElementById( 'content' );
+
+			if ( v !== '' ) {
+
+				window.history.replaceState( {}, '', '?q=' + v + window.location.hash );
+
+				// Show search results, hide navigation
+				searchResults.style.display = 'block';
+				content.style.display = 'none';
+
+				if ( searchData === undefined ) {
+
+					searchResults.innerHTML = '<div style="padding: 16px; color: #999;">Loading search data...</div>';
+					return;
+
+				}
+
+				const regExp = new RegExp( escapeRegExp( v ), 'gi' );
+				const highlightRegExp = new RegExp( v.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ), 'gi' );
+
+				// Search through all categories
+				const results = [];
+				for ( const category in searchData ) {
+
+					const items = searchData[ category ];
+					for ( const item of items ) {
+
+						if ( item.title.match( regExp ) ) {
+
+							results.push( { ...item, category } );
+
+						}
+
+					}
+
+				}
+
+				// Display results
+				if ( results.length > 0 ) {
+
+					// Group results by class
+					const grouped = {};
+					results.forEach( item => {
+
+						const parts = item.title.split( /[#~]/ );
+						const className = parts[ 0 ];
+						const memberName = parts[ 1 ];
+
+						if ( ! grouped[ className ] ) {
+
+							grouped[ className ] = {
+								class: null,
+								members: [],
+								category: item.category
+							};
+
+						}
+
+						// Convert title to hash: "BoxHelper#update" -> "BoxHelper.update"
+						const fullHash = item.title.replace( /[#~]/g, '.' );
+
+						if ( memberName ) {
+
+							if ( memberName.match( regExp ) ) {
+
+								grouped[ className ].members.push( {
+									name: memberName,
+									hash: fullHash,
+									kind: item.kind
+								} );
+
+							}
+
+						} else {
+
+							grouped[ className ].class = {
+								name: className,
+								hash: fullHash
+							};
+
+						}
+
+					} );
+
+					// Helper function to highlight matching text
+					function highlightMatch( text, regExp ) {
+
+						return text.replace( regExp, match => `<strong>${match}</strong>` );
+
+					}
+
+					// Group by category
+					const byCategory = {};
+					for ( const className in grouped ) {
+
+						const group = grouped[ className ];
+						const category = group.category || 'Other';
+
+						if ( ! byCategory[ category ] ) {
+
+							byCategory[ category ] = {};
+
+						}
+
+						byCategory[ category ][ className ] = group;
+
+					}
+
+					// Render grouped results with category headers
+					const currentHash = window.location.hash.substring( 1 );
+					let html = '';
+
+					const categories = Object.keys( searchData );
+					for ( const category of categories ) {
+
+						if ( ! byCategory[ category ] ) continue;
+
+						html += `<h2>${category}</h2>`;
+
+						for ( const className in byCategory[ category ] ) {
+
+							const group = byCategory[ category ][ className ];
+
+							if ( group.class ) {
+
+								html += '<div class="search-result-group">';
+								const selectedClass = group.class.hash === currentHash ? ' selected' : '';
+								const highlightedName = highlightMatch( group.class.name, highlightRegExp );
+								html += `<a href="#${group.class.hash}" class="search-result-class${selectedClass}">${highlightedName}</a>`;
+
+							}
+
+							if ( group.members.length > 0 ) {
+
+								if ( ! group.class ) {
+
+									html += '<div class="search-result-group">';
+									html += `<a href="#${className}" class="search-result-class">${className}</a>`;
+
+								}
+
+								group.members.forEach( member => {
+
+									const selectedClass = member.hash === currentHash ? ' selected' : '';
+									const highlightedName = highlightMatch( member.name, highlightRegExp );
+									const suffix = member.kind === 'function' ? '()' : '';
+									html += `<a href="#${member.hash}" class="search-result-member${selectedClass}">.${highlightedName}${suffix}</a>`;
+
+								} );
+
+							}
+
+							if ( group.class || group.members.length > 0 ) {
+
+								html += '</div>';
+
+							}
+
+						}
+
+					}
+
+					searchResults.innerHTML = html;
+
+					// Add click handlers to update selection
+					searchResults.querySelectorAll( 'a' ).forEach( link => {
+
+						link.addEventListener( 'click', function () {
+
+							// Remove selected class from all links
+							searchResults.querySelectorAll( 'a' ).forEach( item => {
+
+								item.classList.remove( 'selected' );
+
+							} );
+							// Add selected class to clicked link
+							link.classList.add( 'selected' );
+
+						} );
+
+					} );
+
+				} else {
+
+					searchResults.innerHTML = '<div style="padding: 16px; color: #999;">No results found.</div>';
+
+				}
+
+			} else {
+
+				window.history.replaceState( {}, '', window.location.pathname + window.location.hash );
+
+				// Hide search results, show navigation
+				searchResults.style.display = 'none';
+				content.style.display = 'block';
+
+				// Highlight and scroll to current page in navigation
+				const currentHash = window.location.hash.substring( 1 );
+				if ( currentHash ) {
+
+					// Extract the base page name (before the first dot for members)
+					const basePage = currentHash.split( '.' )[ 0 ];
+
+					// Find and highlight the link in navigation
+					const pageInfo = pageLinks[ basePage ];
+					if ( pageInfo ) {
+
+						// Remove selected class from all links
+						navigation.querySelectorAll( 'a' ).forEach( function ( item ) {
+
+							item.classList.remove( 'selected' );
+
+						} );
+
+						// Add selected class to current page
+						pageInfo.linkElement.classList.add( 'selected' );
+
+						// Scroll the link into view
+						pageInfo.linkElement.scrollIntoView( { block: 'center' } );
+
+					}
+
+				}
+
+			}
+
+		}
+
+		// Routing
+
+		function createNewIframe() {
+
+			const hash = window.location.hash.substring( 1 );
+
+			// Parse hash: "global.Break" -> pageName: "global", anchor: "#Break"
+			// or "BoxHelper" -> pageName: "BoxHelper", anchor: ""
+			let pageName, anchor;
+			const dotIndex = hash.indexOf( '.' );
+
+			if ( dotIndex !== - 1 ) {
+
+				pageName = hash.substring( 0, dotIndex );
+				anchor = '#' + hash.substring( dotIndex + 1 );
+
+			} else {
+
+				pageName = hash;
+				anchor = '';
+
+			}
+
+			let subtitle = '';
+
+			const oldIframe = iframe;
+			iframe = oldIframe.cloneNode();
+
+			iframe.style.display = 'none';
+
+			// Try to find the page link - first with full hash (e.g., "global.Break"), then without anchor
+			const fullPageName = hash;
+			let pageLink = pageLinks[ fullPageName ] || pageLinks[ pageName ];
+
+			// If not found and hash doesn't contain a dot, try TSL.{hash} or global.{hash}
+			if ( ! pageLink && dotIndex === - 1 && hash ) {
+
+				pageLink = pageLinks[ 'TSL.' + hash ] || pageLinks[ 'global.' + hash ];
+
+				// Update the hash to the full path
+				if ( pageLink ) {
+
+					const prefix = pageLinks[ 'TSL.' + hash ] ? 'TSL' : 'global';
+					window.history.replaceState( {}, '', window.location.pathname + window.location.search + '#' + prefix + '.' + hash );
+
+				}
+
+			}
+
+			if ( hash && pageLink ) {
+
+				iframe.onload = function () {
+
+					iframe.style.display = 'unset';
+
+					// Intercept clicks on internal documentation links in the iframe
+					setupIframeLinks();
+
+				};
+
+				// Use the stored anchor if available, otherwise use the parsed one
+				const iframeAnchor = pageLink.anchor || anchor;
+				iframe.src = pageLink.pageURL + iframeAnchor;
+				subtitle = hash + ' – ';
+
+				// Update navigation selection and scroll into view
+				navigation.querySelectorAll( 'a' ).forEach( function ( item ) {
+
+					item.classList.remove( 'selected' );
+
+				} );
+
+				if ( pageLink.linkElement ) {
+
+					pageLink.linkElement.classList.add( 'selected' );
+
+					// Only scroll if this is not a user click (user clicks handle their own smooth scrolling)
+					if ( ! isUserClick ) {
+
+						pageLink.linkElement.scrollIntoView( { block: 'center' } );
+
+					}
+
+					isUserClick = false;
+
+				}
+
+			} else {
+
+				iframe.src = '';
+				subtitle = '';
+
+			}
+
+			document.body.replaceChild( iframe, oldIframe );
+			document.title = subtitle + 'three.js docs';
+
+		}
+
+		function setupIframeLinks() {
+
+			try {
+
+				// Get the iframe's document
+				const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+
+				// Find all links in the iframe
+				const links = iframeDoc.querySelectorAll( 'a' );
+
+				links.forEach( function ( link ) {
+
+					link.addEventListener( 'click', function ( event ) {
+
+						const href = link.getAttribute( 'href' );
+
+						// Only handle relative links to .html files (with or without anchors)
+						if ( href && ! href.startsWith( 'http' ) && href.includes( '.html' ) ) {
+
+							event.preventDefault();
+
+							// Parse href: "global.html#Break" -> "global.Break" or "Light.html" -> "Light"
+							const match = href.match( /^([^#]+\.html)(#.*)?$/ );
+							if ( match ) {
+
+								const htmlFile = match[ 1 ];
+								const anchor = match[ 2 ] || '';
+								const pageName = htmlFile.replace( /\.html$/, '' );
+								// Convert to dot notation: "global.html#Break" -> "global.Break"
+								const fullHash = pageName + anchor.replace( '#', '.' );
+
+								// Update the parent page's hash
+								window.location.hash = fullHash;
+
+							}
+
+						}
+
+					} );
+
+				} );
+
+			} catch ( e ) {
+
+				// Ignore cross-origin errors
+				console.error( 'Could not set up iframe links:', e );
+
+			}
+
+		}
+
+		//
+
+		console.log( [
+			'    __     __',
+			' __/ __\\  / __\\__   ____   _____   _____',
+			'/ __/  /\\/ /  /___\\/ ____\\/ _____\\/ _____\\',
+			'\\/_   __/ /   _   / /  __/ / __  / / __  /_   __   _____',
+			'/ /  / / /  / /  / /  / / /  ___/ /  ___/\\ _\\/ __\\/ _____\\',
+			'\\/__/  \\/__/\\/__/\\/__/  \\/_____/\\/_____/\\/__/ /  / /  ___/',
+			'                                         / __/  /  \\__  \\',
+			'                                         \\/____/\\/_____/'
+		].join( '\n' ) );
+
+		</script>
+
+	</body>
+
+</html>

+ 290 - 0
utils/docs/template/static/scenes/bones-browser.html

@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>Three.js Bones Browser</title>
+		<link rel="shortcut icon" href="../../files/favicon.ico" />
+		<link rel="stylesheet" type="text/css" href="../../files/main.css">
+		<style>
+			canvas {
+				display: block;
+				width: 100%;
+				height: 100%;
+			}
+
+			#newWindow {
+				display: block;
+				position: absolute;
+				bottom: 0.3em;
+				left: 0.5em;
+				color: #fff;
+			}
+		</style>
+	</head>
+	<body>
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../../build/three.module.js",
+					"three/addons/": "../../examples/jsm/"
+				}
+			}
+		</script>
+
+		<a id='newWindow' href='./bones-browser.html' target='_blank'>Open in New Window</a>
+
+		<script type="module">
+			import {
+				Bone,
+				Color,
+				CylinderGeometry,
+				DirectionalLight,
+				DoubleSide,
+				Float32BufferAttribute,
+				MeshPhongMaterial,
+				PerspectiveCamera,
+				Scene,
+				SkinnedMesh,
+				Skeleton,
+				SkeletonHelper,
+				Vector3,
+				Uint16BufferAttribute,
+				WebGLRenderer
+			} from 'three';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let gui, scene, camera, renderer, orbit, lights, mesh, bones, skeletonHelper;
+
+			const state = {
+				animateBones: false
+			};
+
+			function initScene() {
+
+				gui = new GUI();
+
+				scene = new Scene();
+				scene.background = new Color( 0x444444 );
+
+				camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 200 );
+				camera.position.z = 30;
+				camera.position.y = 30;
+
+				renderer = new WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				orbit = new OrbitControls( camera, renderer.domElement );
+				orbit.enableZoom = false;
+
+				lights = [];
+				lights[ 0 ] = new DirectionalLight( 0xffffff, 3 );
+				lights[ 1 ] = new DirectionalLight( 0xffffff, 3 );
+				lights[ 2 ] = new DirectionalLight( 0xffffff, 3 );
+
+				lights[ 0 ].position.set( 0, 200, 0 );
+				lights[ 1 ].position.set( 100, 200, 100 );
+				lights[ 2 ].position.set( - 100, - 200, - 100 );
+
+				scene.add( lights[ 0 ] );
+				scene.add( lights[ 1 ] );
+				scene.add( lights[ 2 ] );
+
+				window.addEventListener( 'resize', function () {
+
+					camera.aspect = window.innerWidth / window.innerHeight;
+					camera.updateProjectionMatrix();
+
+					renderer.setSize( window.innerWidth, window.innerHeight );
+
+				}, false );
+
+				initBones();
+				setupDatGui();
+
+			}
+
+			function createGeometry( sizing ) {
+
+				const geometry = new CylinderGeometry(
+					5, // radiusTop
+					5, // radiusBottom
+					sizing.height, // height
+					8, // radiusSegments
+					sizing.segmentCount * 3, // heightSegments
+					true // openEnded
+				);
+
+				const position = geometry.attributes.position;
+
+				const vertex = new Vector3();
+
+				const skinIndices = [];
+				const skinWeights = [];
+
+				for ( let i = 0; i < position.count; i ++ ) {
+
+					vertex.fromBufferAttribute( position, i );
+
+					const y = ( vertex.y + sizing.halfHeight );
+
+					const skinIndex = Math.floor( y / sizing.segmentHeight );
+					const skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight;
+
+					skinIndices.push( skinIndex, skinIndex + 1, 0, 0 );
+					skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 );
+
+				}
+
+				geometry.setAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) );
+				geometry.setAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) );
+
+				return geometry;
+
+			}
+
+			function createBones( sizing ) {
+
+				bones = [];
+
+				let prevBone = new Bone();
+				bones.push( prevBone );
+				prevBone.position.y = - sizing.halfHeight;
+
+				for ( let i = 0; i < sizing.segmentCount; i ++ ) {
+
+					const bone = new Bone();
+					bone.position.y = sizing.segmentHeight;
+					bones.push( bone );
+					prevBone.add( bone );
+					prevBone = bone;
+
+				}
+
+				return bones;
+
+			}
+
+			function createMesh( geometry, bones ) {
+
+				const material = new MeshPhongMaterial( {
+					color: 0x156289,
+					emissive: 0x072534,
+					side: DoubleSide,
+					flatShading: true
+				} );
+
+				const mesh = new SkinnedMesh( geometry,	material );
+				const skeleton = new Skeleton( bones );
+
+				mesh.add( bones[ 0 ] );
+
+				mesh.bind( skeleton );
+
+				skeletonHelper = new SkeletonHelper( mesh );
+				skeletonHelper.material.linewidth = 2;
+				scene.add( skeletonHelper );
+
+				return mesh;
+
+			}
+
+			function setupDatGui() {
+
+				let folder = gui.addFolder( 'General Options' );
+
+				folder.add( state, 'animateBones' );
+				folder.controllers[ 0 ].name( 'Animate Bones' );
+
+				folder.add( mesh, 'pose' );
+				folder.controllers[ 1 ].name( '.pose()' );
+
+				const bones = mesh.skeleton.bones;
+
+				for ( let i = 0; i < bones.length; i ++ ) {
+
+					const bone = bones[ i ];
+
+					folder = gui.addFolder( 'Bone ' + i );
+
+					folder.add( bone.position, 'x', - 10 + bone.position.x, 10 + bone.position.x );
+					folder.add( bone.position, 'y', - 10 + bone.position.y, 10 + bone.position.y );
+					folder.add( bone.position, 'z', - 10 + bone.position.z, 10 + bone.position.z );
+
+					folder.add( bone.rotation, 'x', - Math.PI * 0.5, Math.PI * 0.5 );
+					folder.add( bone.rotation, 'y', - Math.PI * 0.5, Math.PI * 0.5 );
+					folder.add( bone.rotation, 'z', - Math.PI * 0.5, Math.PI * 0.5 );
+
+					folder.add( bone.scale, 'x', 0, 2 );
+					folder.add( bone.scale, 'y', 0, 2 );
+					folder.add( bone.scale, 'z', 0, 2 );
+
+					folder.controllers[ 0 ].name( 'position.x' );
+					folder.controllers[ 1 ].name( 'position.y' );
+					folder.controllers[ 2 ].name( 'position.z' );
+
+					folder.controllers[ 3 ].name( 'rotation.x' );
+					folder.controllers[ 4 ].name( 'rotation.y' );
+					folder.controllers[ 5 ].name( 'rotation.z' );
+
+					folder.controllers[ 6 ].name( 'scale.x' );
+					folder.controllers[ 7 ].name( 'scale.y' );
+					folder.controllers[ 8 ].name( 'scale.z' );
+
+				}
+
+			}
+
+			function initBones() {
+
+				const segmentHeight = 8;
+				const segmentCount = 4;
+				const height = segmentHeight * segmentCount;
+				const halfHeight = height * 0.5;
+
+				const sizing = {
+					segmentHeight: segmentHeight,
+					segmentCount: segmentCount,
+					height: height,
+					halfHeight: halfHeight
+				};
+
+				const geometry = createGeometry( sizing );
+				const bones = createBones( sizing );
+				mesh = createMesh( geometry, bones );
+
+				mesh.scale.multiplyScalar( 1 );
+				scene.add( mesh );
+
+			}
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				const time = Date.now() * 0.001;
+
+				//Wiggle the bones
+				if ( state.animateBones ) {
+
+					for ( let i = 0; i < mesh.skeleton.bones.length; i ++ ) {
+
+						mesh.skeleton.bones[ i ].rotation.z = Math.sin( time ) * 2 / mesh.skeleton.bones.length;
+
+					}
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+			initScene();
+			render();
+
+		</script>
+	</body>
+</html>

+ 285 - 0
utils/docs/template/static/scenes/ccdiksolver-browser.html

@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>Three.js CCDIKSolver Browser</title>
+		<link rel="shortcut icon" href="../../files/favicon.ico" />
+		<link rel="stylesheet" type="text/css" href="../../files/main.css">
+		<style>
+			canvas {
+				display: block;
+				width: 100%;
+				height: 100%;
+			}
+
+			#newWindow {
+				display: block;
+				position: absolute;
+				bottom: 0.3em;
+				left: 0.5em;
+				color: #fff;
+			}
+		</style>
+	</head>
+	<body>
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../../build/three.module.js",
+					"three/addons/": "../../examples/jsm/"
+				}
+			}
+		</script>
+
+		<a id='newWindow' href='./ccdiksolver-browser.html' target='_blank'>Open in New Window</a>
+
+		<script type="module">
+			//
+			// Forked from /docs/api/en/objects/SkinnedMesh example
+			//
+
+			import {
+				Bone,
+				Color,
+				CylinderGeometry,
+				DoubleSide,
+				Float32BufferAttribute,
+				MeshPhongMaterial,
+				PerspectiveCamera,
+				Scene,
+				SkinnedMesh,
+				Skeleton,
+				SkeletonHelper,
+				Vector3,
+				Uint16BufferAttribute,
+				WebGLRenderer
+			} from 'three';
+			import { CCDIKSolver, CCDIKHelper } from 'three/addons/animation/CCDIKSolver.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let gui, scene, camera, renderer, orbit, mesh, bones, skeletonHelper, ikSolver;
+
+			const state = {
+				ikSolverAutoUpdate: true
+			};
+
+			function initScene() {
+
+				gui = new GUI();
+
+				scene = new Scene();
+				scene.background = new Color( 0x444444 );
+
+				camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 200 );
+				camera.position.z = 30;
+				camera.position.y = 30;
+
+				renderer = new WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				orbit = new OrbitControls( camera, renderer.domElement );
+				orbit.enableZoom = false;
+
+				window.addEventListener( 'resize', function () {
+
+					camera.aspect = window.innerWidth / window.innerHeight;
+					camera.updateProjectionMatrix();
+
+					renderer.setSize( window.innerWidth, window.innerHeight );
+
+				}, false );
+
+				initBones();
+				setupDatGui();
+
+			}
+
+			function createGeometry( sizing ) {
+
+				const geometry = new CylinderGeometry(
+					5, // radiusTop
+					5, // radiusBottom
+					sizing.height, // height
+					8, // radiusSegments
+					sizing.segmentCount * 1, // heightSegments
+					true // openEnded
+				);
+
+				const position = geometry.attributes.position;
+
+				const vertex = new Vector3();
+
+				const skinIndices = [];
+				const skinWeights = [];
+
+				for ( let i = 0; i < position.count; i ++ ) {
+
+					vertex.fromBufferAttribute( position, i );
+
+					const y = ( vertex.y + sizing.halfHeight );
+
+					const skinIndex = Math.floor( y / sizing.segmentHeight );
+					const skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight;
+
+					skinIndices.push( skinIndex, skinIndex + 1, 0, 0 );
+					skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 );
+
+				}
+
+				geometry.setAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) );
+				geometry.setAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) );
+
+				return geometry;
+
+			}
+
+			function createBones( sizing ) {
+
+				bones = [];
+
+				// "root bone"
+				const rootBone = new Bone();
+				rootBone.name = 'root';
+				rootBone.position.y = - sizing.halfHeight;
+				bones.push( rootBone );
+
+				//
+				// "bone0", "bone1", "bone2", "bone3"
+				//
+
+				// "bone0"
+				let prevBone = new Bone();
+				prevBone.position.y = 0;
+				rootBone.add( prevBone );
+				bones.push( prevBone );
+
+				// "bone1", "bone2", "bone3"
+				for ( let i = 1; i <= sizing.segmentCount; i ++ ) {
+
+					const bone = new Bone();
+					bone.position.y = sizing.segmentHeight;
+					bones.push( bone );
+					bone.name = `bone${i}`;
+					prevBone.add( bone );
+					prevBone = bone;
+
+				}
+
+				// "target"
+				const targetBone = new Bone();
+				targetBone.name = 'target';
+				targetBone.position.y = sizing.height + sizing.segmentHeight; // relative to parent: rootBone
+				rootBone.add( targetBone );
+				bones.push( targetBone );
+
+				return bones;
+
+			}
+
+			function createMesh( geometry, bones ) {
+
+				const material = new MeshPhongMaterial( {
+					color: 0x156289,
+					emissive: 0x072534,
+					side: DoubleSide,
+					flatShading: true,
+					wireframe: true
+				} );
+
+				const mesh = new SkinnedMesh( geometry,	material );
+				const skeleton = new Skeleton( bones );
+
+				mesh.add( bones[ 0 ] );
+
+				mesh.bind( skeleton );
+
+				skeletonHelper = new SkeletonHelper( mesh );
+				skeletonHelper.material.linewidth = 2;
+				scene.add( skeletonHelper );
+
+				return mesh;
+
+			}
+
+			function setupDatGui() {
+
+				gui.add( mesh, 'pose' ).name( 'mesh.pose()' );
+
+				mesh.skeleton.bones
+					.filter( ( bone ) => bone.name === 'target' )
+					.forEach( function ( bone ) {
+
+						const folder = gui.addFolder( bone.name );
+
+						const delta = 20;
+						folder.add( bone.position, 'x', - delta + bone.position.x, delta + bone.position.x );
+						folder.add( bone.position, 'y', - bone.position.y, bone.position.y );
+						folder.add( bone.position, 'z', - delta + bone.position.z, delta + bone.position.z );
+
+		} );
+
+				gui.add( ikSolver, 'update' ).name( 'ikSolver.update()' );
+				gui.add( state, 'ikSolverAutoUpdate' );
+
+			}
+
+			function initBones() {
+
+				const segmentHeight = 8;
+				const segmentCount = 3;
+				const height = segmentHeight * segmentCount;
+				const halfHeight = height * 0.5;
+
+				const sizing = {
+					segmentHeight,
+					segmentCount,
+					height,
+					halfHeight
+				};
+
+				const geometry = createGeometry( sizing );
+				const bones = createBones( sizing );
+				mesh = createMesh( geometry, bones );
+
+				scene.add( mesh );
+
+				//
+				// ikSolver
+				//
+
+				const iks = [
+					{
+						target: 5,
+						effector: 4,
+						links: [ { index: 3 }, { index: 2 }, { index: 1 } ]
+					}
+				];
+				ikSolver = new CCDIKSolver( mesh, iks );
+				scene.add( new CCDIKHelper( mesh, iks ) );
+
+			}
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				if ( state.ikSolverAutoUpdate ) {
+
+					ikSolver?.update();
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+			initScene();
+			render();
+
+		</script>
+	</body>
+</html>

+ 806 - 0
utils/docs/template/static/scenes/geometry-browser.html

@@ -0,0 +1,806 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>Three.js Geometry Browser</title>
+		<link rel="shortcut icon" href="../../files/favicon.ico" />
+		<link rel="stylesheet" type="text/css" href="../../files/main.css">
+		<style>
+			canvas {
+				display: block;
+				width: 100%;
+				height: 100%;
+			}
+
+			#newWindow {
+				display: block;
+				position: absolute;
+				bottom: 0.3em;
+				left: 0.5em;
+				color: #fff;
+			}
+		</style>
+	</head>
+	<body>
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../../build/three.module.js",
+					"three/addons/": "../../examples/jsm/"
+				}
+			}
+		</script>
+
+		<a id='newWindow' href='./geometry-browser.html' target='_blank'>Open in New Window</a>
+
+		<script type="module">
+			import {
+				BoxGeometry,
+				BufferGeometry,
+				CapsuleGeometry,
+				CircleGeometry,
+				Color,
+				ConeGeometry,
+				Curve,
+				CylinderGeometry,
+				DirectionalLight,
+				DodecahedronGeometry,
+				DoubleSide,
+				ExtrudeGeometry,
+				Float32BufferAttribute,
+				Group,
+				IcosahedronGeometry,
+				LatheGeometry,
+				LineSegments,
+				LineBasicMaterial,
+				Mesh,
+				MeshPhongMaterial,
+				OctahedronGeometry,
+				PerspectiveCamera,
+				PlaneGeometry,
+				RingGeometry,
+				Scene,
+				Shape,
+				ShapeGeometry,
+				SphereGeometry,
+				TetrahedronGeometry,
+				TorusGeometry,
+				TorusKnotGeometry,
+				TubeGeometry,
+				Vector2,
+				Vector3,
+				WireframeGeometry,
+				WebGLRenderer
+			} from 'three';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			const twoPi = Math.PI * 2;
+
+			class CustomSinCurve extends Curve {
+
+				constructor( scale = 1 ) {
+
+					super();
+
+					this.scale = scale;
+
+				}
+
+				getPoint( t, optionalTarget = new Vector3() ) {
+
+					const tx = t * 3 - 1.5;
+					const ty = Math.sin( 2 * Math.PI * t );
+					const tz = 0;
+
+					return optionalTarget.set( tx, ty, tz ).multiplyScalar( this.scale );
+
+				}
+
+			}
+
+			function updateGroupGeometry( mesh, geometry ) {
+
+				mesh.children[ 0 ].geometry.dispose();
+				mesh.children[ 1 ].geometry.dispose();
+
+				mesh.children[ 0 ].geometry = new WireframeGeometry( geometry );
+				mesh.children[ 1 ].geometry = geometry;
+
+				// these do not update nicely together if shared
+
+			}
+
+			// heart shape
+
+			const x = 0, y = 0;
+
+			const heartShape = new Shape();
+
+			heartShape.moveTo( x + 5, y + 5 );
+			heartShape.bezierCurveTo( x + 5, y + 5, x + 4, y, x, y );
+			heartShape.bezierCurveTo( x - 6, y, x - 6, y + 7, x - 6, y + 7 );
+			heartShape.bezierCurveTo( x - 6, y + 11, x - 3, y + 15.4, x + 5, y + 19 );
+			heartShape.bezierCurveTo( x + 12, y + 15.4, x + 16, y + 11, x + 16, y + 7 );
+			heartShape.bezierCurveTo( x + 16, y + 7, x + 16, y, x + 10, y );
+			heartShape.bezierCurveTo( x + 7, y, x + 5, y + 5, x + 5, y + 5 );
+
+			const guis = {
+
+				BoxGeometry: function ( mesh ) {
+
+					const data = {
+						width: 15,
+						height: 15,
+						depth: 15,
+						widthSegments: 1,
+						heightSegments: 1,
+						depthSegments: 1
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new BoxGeometry(
+								data.width, data.height, data.depth, data.widthSegments, data.heightSegments, data.depthSegments
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.BoxGeometry' );
+
+					folder.add( data, 'width', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'height', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'depth', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'widthSegments', 1, 10 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 1, 10 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'depthSegments', 1, 10 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				CapsuleGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 5,
+						height: 5,
+						capSegments: 10,
+						radialSegments: 20,
+						heightSegments: 1
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new CapsuleGeometry( data.radius, data.height, data.capSegments, data.radialSegments, data.heightSegments ),
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.CapsuleGeometry' );
+
+					folder.add( data, 'radius', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'height', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'capSegments', 1, 32 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 1, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 1, 64 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				CylinderGeometry: function ( mesh ) {
+
+					const data = {
+						radiusTop: 5,
+						radiusBottom: 5,
+						height: 10,
+						radialSegments: 8,
+						heightSegments: 1,
+						openEnded: false,
+						thetaStart: 0,
+						thetaLength: twoPi
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new CylinderGeometry(
+								data.radiusTop,
+								data.radiusBottom,
+								data.height,
+								data.radialSegments,
+								data.heightSegments,
+								data.openEnded,
+								data.thetaStart,
+								data.thetaLength
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.CylinderGeometry' );
+
+					folder.add( data, 'radiusTop', 0, 30 ).onChange( generateGeometry );
+					folder.add( data, 'radiusBottom', 0, 30 ).onChange( generateGeometry );
+					folder.add( data, 'height', 1, 50 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 3, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 1, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'openEnded' ).onChange( generateGeometry );
+					folder.add( data, 'thetaStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaLength', 0, twoPi ).onChange( generateGeometry );
+
+
+					generateGeometry();
+
+				},
+
+				ConeGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 5,
+						height: 10,
+						radialSegments: 8,
+						heightSegments: 1,
+						openEnded: false,
+						thetaStart: 0,
+						thetaLength: twoPi
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new ConeGeometry(
+								data.radius,
+								data.height,
+								data.radialSegments,
+								data.heightSegments,
+								data.openEnded,
+								data.thetaStart,
+								data.thetaLength
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.ConeGeometry' );
+
+					folder.add( data, 'radius', 0, 30 ).onChange( generateGeometry );
+					folder.add( data, 'height', 1, 50 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 3, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 1, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'openEnded' ).onChange( generateGeometry );
+					folder.add( data, 'thetaStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaLength', 0, twoPi ).onChange( generateGeometry );
+
+
+					generateGeometry();
+
+				},
+
+				CircleGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						segments: 32,
+						thetaStart: 0,
+						thetaLength: twoPi
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new CircleGeometry(
+								data.radius, data.segments, data.thetaStart, data.thetaLength
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.CircleGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'segments', 0, 128 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'thetaStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaLength', 0, twoPi ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				DodecahedronGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						detail: 0
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new DodecahedronGeometry(
+								data.radius, data.detail
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.DodecahedronGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'detail', 0, 5 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				IcosahedronGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						detail: 0
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new IcosahedronGeometry(
+								data.radius, data.detail
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.IcosahedronGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'detail', 0, 5 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				LatheGeometry: function ( mesh ) {
+
+					const points = [];
+
+					for ( let i = 0; i < 10; i ++ ) {
+
+						points.push( new Vector2( Math.sin( i * 0.2 ) * 10 + 5, ( i - 5 ) * 2 ) );
+
+					}
+
+					const data = {
+						segments: 12,
+						phiStart: 0,
+						phiLength: twoPi
+					};
+
+					function generateGeometry() {
+
+						const geometry = new LatheGeometry(
+							points, data.segments, data.phiStart, data.phiLength
+						);
+
+						updateGroupGeometry( mesh, geometry );
+
+					}
+
+					const folder = gui.addFolder( 'THREE.LatheGeometry' );
+
+					folder.add( data, 'segments', 1, 30 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'phiStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'phiLength', 0, twoPi ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				OctahedronGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						detail: 0
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new OctahedronGeometry(
+								data.radius, data.detail
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.OctahedronGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'detail', 0, 5 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				PlaneGeometry: function ( mesh ) {
+
+					const data = {
+						width: 10,
+						height: 10,
+						widthSegments: 1,
+						heightSegments: 1
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new PlaneGeometry(
+								data.width, data.height, data.widthSegments, data.heightSegments
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.PlaneGeometry' );
+
+					folder.add( data, 'width', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'height', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'widthSegments', 1, 30 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 1, 30 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				RingGeometry: function ( mesh ) {
+
+					const data = {
+						innerRadius: 5,
+						outerRadius: 10,
+						thetaSegments: 8,
+						phiSegments: 8,
+						thetaStart: 0,
+						thetaLength: twoPi
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new RingGeometry(
+								data.innerRadius, data.outerRadius, data.thetaSegments, data.phiSegments, data.thetaStart, data.thetaLength
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.RingGeometry' );
+
+					folder.add( data, 'innerRadius', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'outerRadius', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'thetaSegments', 1, 30 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'phiSegments', 1, 30 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'thetaStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaLength', 0, twoPi ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				SphereGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 15,
+						widthSegments: 32,
+						heightSegments: 16,
+						phiStart: 0,
+						phiLength: twoPi,
+						thetaStart: 0,
+						thetaLength: Math.PI
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new SphereGeometry(
+								data.radius, data.widthSegments, data.heightSegments, data.phiStart, data.phiLength, data.thetaStart, data.thetaLength
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.SphereGeometry' );
+
+					folder.add( data, 'radius', 1, 30 ).onChange( generateGeometry );
+					folder.add( data, 'widthSegments', 3, 64 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'heightSegments', 2, 32 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'phiStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'phiLength', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaStart', 0, twoPi ).onChange( generateGeometry );
+					folder.add( data, 'thetaLength', 0, twoPi ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				TetrahedronGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						detail: 0
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new TetrahedronGeometry(
+								data.radius, data.detail
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.TetrahedronGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'detail', 0, 5 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				TorusGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						tube: 3,
+						radialSegments: 16,
+						tubularSegments: 100,
+						arc: twoPi
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new TorusGeometry(
+								data.radius, data.tube, data.radialSegments, data.tubularSegments, data.arc
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.TorusGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'tube', 0.1, 10 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 2, 30 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'tubularSegments', 3, 200 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'arc', 0.1, twoPi ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				TorusKnotGeometry: function ( mesh ) {
+
+					const data = {
+						radius: 10,
+						tube: 3,
+						tubularSegments: 64,
+						radialSegments: 8,
+						p: 2,
+						q: 3
+					};
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new TorusKnotGeometry(
+								data.radius, data.tube, data.tubularSegments, data.radialSegments,
+								data.p, data.q
+							)
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.TorusKnotGeometry' );
+
+					folder.add( data, 'radius', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'tube', 0.1, 10 ).onChange( generateGeometry );
+					folder.add( data, 'tubularSegments', 3, 300 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 3, 20 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'p', 1, 20 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'q', 1, 20 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				TubeGeometry: function ( mesh ) {
+
+					const data = {
+						segments: 20,
+						radius: 2,
+						radialSegments: 8
+					};
+
+					const path = new CustomSinCurve( 10 );
+
+					function generateGeometry() {
+
+						updateGroupGeometry( mesh,
+							new TubeGeometry( path, data.segments, data.radius, data.radialSegments, false )
+						);
+
+					}
+
+					const folder = gui.addFolder( 'THREE.TubeGeometry' );
+
+					folder.add( data, 'segments', 1, 100 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'radius', 1, 10 ).onChange( generateGeometry );
+					folder.add( data, 'radialSegments', 1, 20 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				ShapeGeometry: function ( mesh ) {
+
+					const data = {
+						segments: 12
+					};
+
+					function generateGeometry() {
+
+						const geometry = new ShapeGeometry( heartShape, data.segments );
+						geometry.center();
+
+						updateGroupGeometry( mesh, geometry );
+
+					}
+
+					const folder = gui.addFolder( 'THREE.ShapeGeometry' );
+					folder.add( data, 'segments', 1, 100 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				},
+
+				ExtrudeGeometry: function ( mesh ) {
+
+					const data = {
+						steps: 2,
+						depth: 16,
+						bevelEnabled: true,
+						bevelThickness: 1,
+						bevelSize: 1,
+						bevelOffset: 0,
+						bevelSegments: 1
+					};
+
+					const length = 12, width = 8;
+
+					const shape = new Shape();
+					shape.moveTo( 0, 0 );
+					shape.lineTo( 0, width );
+					shape.lineTo( length, width );
+					shape.lineTo( length, 0 );
+					shape.lineTo( 0, 0 );
+
+					function generateGeometry() {
+
+						const geometry = new ExtrudeGeometry( shape, data );
+						geometry.center();
+
+						updateGroupGeometry( mesh, geometry );
+
+					}
+
+					const folder = gui.addFolder( 'THREE.ExtrudeGeometry' );
+
+					folder.add( data, 'steps', 1, 10 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'depth', 1, 20 ).onChange( generateGeometry );
+					folder.add( data, 'bevelThickness', 1, 5 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'bevelSize', 0, 5 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'bevelOffset', - 4, 5 ).step( 1 ).onChange( generateGeometry );
+					folder.add( data, 'bevelSegments', 1, 5 ).step( 1 ).onChange( generateGeometry );
+
+					generateGeometry();
+
+				}
+
+			};
+
+			function chooseFromHash( mesh ) {
+
+				const selectedGeometry = window.location.hash.substring( 1 ) || 'TorusGeometry';
+
+				if ( guis[ selectedGeometry ] !== undefined ) {
+
+					guis[ selectedGeometry ]( mesh );
+
+				}
+
+			}
+
+			//
+
+			const selectedGeometry = window.location.hash.substring( 1 );
+
+			if ( guis[ selectedGeometry ] !== undefined ) {
+
+				document.getElementById( 'newWindow' ).href += '#' + selectedGeometry;
+
+			}
+
+			const gui = new GUI();
+
+			const scene = new Scene();
+			scene.background = new Color( 0x444444 );
+
+			const camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
+			camera.position.z = 30;
+
+			const renderer = new WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			document.body.appendChild( renderer.domElement );
+
+			const orbit = new OrbitControls( camera, renderer.domElement );
+			orbit.enableZoom = false;
+
+			const lights = [];
+			lights[ 0 ] = new DirectionalLight( 0xffffff, 3 );
+			lights[ 1 ] = new DirectionalLight( 0xffffff, 3 );
+			lights[ 2 ] = new DirectionalLight( 0xffffff, 3 );
+
+			lights[ 0 ].position.set( 0, 200, 0 );
+			lights[ 1 ].position.set( 100, 200, 100 );
+			lights[ 2 ].position.set( - 100, - 200, - 100 );
+
+			scene.add( lights[ 0 ] );
+			scene.add( lights[ 1 ] );
+			scene.add( lights[ 2 ] );
+
+			const group = new Group();
+
+			const geometry = new BufferGeometry();
+			geometry.setAttribute( 'position', new Float32BufferAttribute( [], 3 ) );
+
+			const lineMaterial = new LineBasicMaterial( { color: 0xffffff, transparent: true, opacity: 0.5 } );
+			const meshMaterial = new MeshPhongMaterial( { color: 0x156289, emissive: 0x072534, side: DoubleSide, flatShading: true } );
+
+			group.add( new LineSegments( geometry, lineMaterial ) );
+			group.add( new Mesh( geometry, meshMaterial ) );
+
+			chooseFromHash( group );
+
+			scene.add( group );
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				group.rotation.x += 0.005;
+				group.rotation.y += 0.005;
+
+				renderer.render( scene, camera );
+
+			}
+
+			window.addEventListener( 'resize', function () {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}, false );
+
+			render();
+
+		</script>
+	</body>
+</html>

+ 818 - 0
utils/docs/template/static/scenes/material-browser.html

@@ -0,0 +1,818 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>Three.js Material Browser</title>
+		<link rel="shortcut icon" href="../../files/favicon.ico" />
+		<link rel="stylesheet" type="text/css" href="../../files/main.css">
+		<style>
+			canvas {
+				display: block;
+				width: 100%;
+				height: 100%;
+			}
+
+			#newWindow {
+				display: block;
+				position: absolute;
+				bottom: 0.3em;
+				left: 0.5em;
+				color: #fff;
+			}
+		</style>
+	</head>
+	<body>
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../../build/three.module.js",
+					"three/addons/": "../../examples/jsm/"
+				}
+			}
+		</script>
+
+		<a id='newWindow' href='./material-browser.html' target='_blank'>Open in New Window</a>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
+
+			const constants = {
+
+				combine: {
+
+					'THREE.MultiplyOperation': THREE.MultiplyOperation,
+					'THREE.MixOperation': THREE.MixOperation,
+					'THREE.AddOperation': THREE.AddOperation
+
+				},
+
+				side: {
+
+					'THREE.FrontSide': THREE.FrontSide,
+					'THREE.BackSide': THREE.BackSide,
+					'THREE.DoubleSide': THREE.DoubleSide
+
+				},
+
+				blendingMode: {
+
+					'THREE.NoBlending': THREE.NoBlending,
+					'THREE.NormalBlending': THREE.NormalBlending,
+					'THREE.AdditiveBlending': THREE.AdditiveBlending,
+					'THREE.SubtractiveBlending': THREE.SubtractiveBlending,
+					'THREE.MultiplyBlending': THREE.MultiplyBlending,
+					'THREE.CustomBlending': THREE.CustomBlending
+
+				},
+
+				equations: {
+
+					'THREE.AddEquation': THREE.AddEquation,
+					'THREE.SubtractEquation': THREE.SubtractEquation,
+					'THREE.ReverseSubtractEquation': THREE.ReverseSubtractEquation
+
+				},
+
+				destinationFactors: {
+
+					'THREE.ZeroFactor': THREE.ZeroFactor,
+					'THREE.OneFactor': THREE.OneFactor,
+					'THREE.SrcColorFactor': THREE.SrcColorFactor,
+					'THREE.OneMinusSrcColorFactor': THREE.OneMinusSrcColorFactor,
+					'THREE.SrcAlphaFactor': THREE.SrcAlphaFactor,
+					'THREE.OneMinusSrcAlphaFactor': THREE.OneMinusSrcAlphaFactor,
+					'THREE.DstAlphaFactor': THREE.DstAlphaFactor,
+					'THREE.OneMinusDstAlphaFactor': THREE.OneMinusDstAlphaFactor
+
+				},
+
+				sourceFactors: {
+
+					'THREE.DstColorFactor': THREE.DstColorFactor,
+					'THREE.OneMinusDstColorFactor': THREE.OneMinusDstColorFactor,
+					'THREE.SrcAlphaSaturateFactor': THREE.SrcAlphaSaturateFactor
+
+				}
+
+			};
+
+			function getObjectsKeys( obj ) {
+
+				const keys = [];
+
+				for ( const key in obj ) {
+
+					if ( obj.hasOwnProperty( key ) ) {
+
+						keys.push( key );
+
+					}
+
+				}
+
+				return keys;
+
+			}
+
+			const textureLoader = new THREE.TextureLoader();
+			const cubeTextureLoader = new THREE.CubeTextureLoader();
+
+			const envMaps = ( function () {
+
+				const path = '../../examples/textures/cube/SwedishRoyalCastle/';
+				const format = '.jpg';
+				const urls = [
+					path + 'px' + format, path + 'nx' + format,
+					path + 'py' + format, path + 'ny' + format,
+					path + 'pz' + format, path + 'nz' + format
+				];
+
+				const reflectionCube = cubeTextureLoader.load( urls );
+
+				const refractionCube = cubeTextureLoader.load( urls );
+				refractionCube.mapping = THREE.CubeRefractionMapping;
+
+				return {
+					none: null,
+					reflection: reflectionCube,
+					refraction: refractionCube
+				};
+
+			} )();
+
+			const diffuseMaps = ( function () {
+
+				const bricks = textureLoader.load( '../../examples/textures/brick_diffuse.jpg' );
+				bricks.wrapS = THREE.RepeatWrapping;
+				bricks.wrapT = THREE.RepeatWrapping;
+				bricks.repeat.set( 9, 1 );
+
+				return {
+					none: null,
+					bricks: bricks
+				};
+
+			} )();
+
+			const roughnessMaps = ( function () {
+
+				const bricks = textureLoader.load( '../../examples/textures/brick_roughness.jpg' );
+				bricks.wrapT = THREE.RepeatWrapping;
+				bricks.wrapS = THREE.RepeatWrapping;
+				bricks.repeat.set( 9, 1 );
+
+				return {
+					none: null,
+					bricks: bricks
+				};
+
+			} )();
+
+			const matcaps = ( function () {
+
+				return {
+					none: null,
+					porcelainWhite: textureLoader.load( '../../examples/textures/matcaps/matcap-porcelain-white.jpg' )
+				};
+
+			} )();
+
+			const alphaMaps = ( function () {
+
+				const fibers = textureLoader.load( '../../examples/textures/alphaMap.jpg' );
+				fibers.wrapT = THREE.RepeatWrapping;
+				fibers.wrapS = THREE.RepeatWrapping;
+				fibers.repeat.set( 9, 1 );
+
+				return {
+					none: null,
+					fibers: fibers
+				};
+
+			} )();
+
+			const gradientMaps = ( function () {
+
+				const threeTone = textureLoader.load( '../../examples/textures/gradientMaps/threeTone.jpg' );
+				threeTone.minFilter = THREE.NearestFilter;
+				threeTone.magFilter = THREE.NearestFilter;
+
+				const fiveTone = textureLoader.load( '../../examples/textures/gradientMaps/fiveTone.jpg' );
+				fiveTone.minFilter = THREE.NearestFilter;
+				fiveTone.magFilter = THREE.NearestFilter;
+
+				return {
+					none: null,
+					threeTone: threeTone,
+					fiveTone: fiveTone
+				};
+
+			} )();
+
+			const envMapKeys = getObjectsKeys( envMaps );
+			const envMapKeysPBR = envMapKeys.slice( 0, 2 );
+			const diffuseMapKeys = getObjectsKeys( diffuseMaps );
+			const roughnessMapKeys = getObjectsKeys( roughnessMaps );
+			const matcapKeys = getObjectsKeys( matcaps );
+			const alphaMapKeys = getObjectsKeys( alphaMaps );
+			const gradientMapKeys = getObjectsKeys( gradientMaps );
+
+			function generateVertexColors( geometry ) {
+
+				const positionAttribute = geometry.attributes.position;
+
+				const colors = [];
+				const color = new THREE.Color();
+
+				for ( let i = 0, il = positionAttribute.count; i < il; i ++ ) {
+
+					color.setHSL( i / il * Math.random(), 0.5, 0.5 );
+					colors.push( color.r, color.g, color.b );
+
+				}
+
+				geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
+
+			}
+
+			function handleColorChange( color ) {
+
+				return function ( value ) {
+
+					if ( typeof value === 'string' ) {
+
+						value = value.replace( '#', '0x' );
+
+					}
+
+					color.setHex( value );
+
+				};
+
+			}
+
+			function needsUpdate( material, geometry ) {
+
+				return function () {
+
+					material.side = parseInt( material.side ); //Ensure number
+					material.needsUpdate = true;
+					geometry.attributes.position.needsUpdate = true;
+					geometry.attributes.normal.needsUpdate = true;
+					geometry.attributes.color.needsUpdate = true;
+
+				};
+
+			}
+
+			function updateCombine( material ) {
+
+				return function ( combine ) {
+
+					material.combine = parseInt( combine );
+					material.needsUpdate = true;
+
+				};
+
+			}
+
+			function updateTexture( material, materialKey, textures ) {
+
+				return function ( key ) {
+
+					material[ materialKey ] = textures[ key ];
+					material.needsUpdate = true;
+
+				};
+
+			}
+
+			function guiScene( gui, scene ) {
+
+				const folder = gui.addFolder( 'Scene' );
+
+				const data = {
+					background: '#000000',
+					'ambient light': ambientLight.color.getHex()
+				};
+
+				folder.addColor( data, 'ambient light' ).onChange( handleColorChange( ambientLight.color ) );
+
+				guiSceneFog( folder, scene );
+
+			}
+
+			function guiSceneFog( folder, scene ) {
+
+				const fogFolder = folder.addFolder( 'scene.fog' );
+
+				const fog = new THREE.Fog( 0x3f7b9d, 0, 60 );
+
+				const data = {
+					fog: {
+						'THREE.Fog()': false,
+						'scene.fog.color': fog.color.getHex()
+					}
+				};
+
+				fogFolder.add( data.fog, 'THREE.Fog()' ).onChange( function ( useFog ) {
+
+					if ( useFog ) {
+
+						scene.fog = fog;
+
+					} else {
+
+						scene.fog = null;
+
+					}
+
+				} );
+
+				fogFolder.addColor( data.fog, 'scene.fog.color' ).onChange( handleColorChange( fog.color ) );
+
+			}
+
+			function guiMaterial( gui, mesh, material, geometry ) {
+
+				const folder = gui.addFolder( 'THREE.Material' );
+
+				folder.add( material, 'transparent' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'opacity', 0, 1 ).step( 0.01 );
+				// folder.add( material, 'blending', constants.blendingMode );
+				// folder.add( material, 'blendSrc', constants.destinationFactors );
+				// folder.add( material, 'blendDst', constants.destinationFactors );
+				// folder.add( material, 'blendEquation', constants.equations );
+				folder.add( material, 'depthTest' );
+				folder.add( material, 'depthWrite' );
+				// folder.add( material, 'polygonOffset' );
+				// folder.add( material, 'polygonOffsetFactor' );
+				// folder.add( material, 'polygonOffsetUnits' );
+				folder.add( material, 'alphaTest', 0, 1 ).step( 0.01 ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'alphaHash' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'visible' );
+				folder.add( material, 'side', constants.side ).onChange( needsUpdate( material, geometry ) );
+
+			}
+
+			function guiMeshBasicMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					envMaps: envMapKeys[ 0 ],
+					map: diffuseMapKeys[ 0 ],
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshBasicMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+
+				folder.add( data, 'envMaps', envMapKeys ).onChange( updateTexture( material, 'envMap', envMaps ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+				folder.add( material, 'combine', constants.combine ).onChange( updateCombine( material ) );
+				folder.add( material, 'reflectivity', 0, 1 );
+				folder.add( material, 'refractionRatio', 0, 1 );
+
+			}
+
+			function guiMeshDepthMaterial( gui, mesh, material ) {
+
+				const data = {
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshDepthMaterial' );
+
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+
+			}
+
+			function guiMeshNormalMaterial( gui, mesh, material, geometry ) {
+
+				const folder = gui.addFolder( 'THREE.MeshNormalMaterial' );
+
+				folder.add( material, 'flatShading' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+
+			}
+
+			function guiLineBasicMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex()
+				};
+
+				const folder = gui.addFolder( 'THREE.LineBasicMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.add( material, 'linewidth', 0, 10 );
+				folder.add( material, 'linecap', [ 'butt', 'round', 'square' ] );
+				folder.add( material, 'linejoin', [ 'round', 'bevel', 'miter' ] );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+
+			}
+
+			function guiMeshLambertMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					emissive: material.emissive.getHex(),
+					envMaps: envMapKeys[ 0 ],
+					map: diffuseMapKeys[ 0 ],
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshLambertMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.addColor( data, 'emissive' ).onChange( handleColorChange( material.emissive ) );
+
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+
+				folder.add( data, 'envMaps', envMapKeys ).onChange( updateTexture( material, 'envMap', envMaps ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+				folder.add( material, 'combine', constants.combine ).onChange( updateCombine( material ) );
+				folder.add( material, 'reflectivity', 0, 1 );
+				folder.add( material, 'refractionRatio', 0, 1 );
+
+			}
+
+			function guiMeshMatcapMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					matcap: matcapKeys[ 1 ],
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshMatcapMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+
+				folder.add( material, 'flatShading' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( data, 'matcap', matcapKeys ).onChange( updateTexture( material, 'matcap', matcaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+
+			}
+
+			function guiMeshPhongMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					emissive: material.emissive.getHex(),
+					specular: material.specular.getHex(),
+					envMaps: envMapKeys[ 0 ],
+					map: diffuseMapKeys[ 0 ],
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshPhongMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.addColor( data, 'emissive' ).onChange( handleColorChange( material.emissive ) );
+				folder.addColor( data, 'specular' ).onChange( handleColorChange( material.specular ) );
+
+				folder.add( material, 'shininess', 0, 100 );
+				folder.add( material, 'flatShading' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( data, 'envMaps', envMapKeys ).onChange( updateTexture( material, 'envMap', envMaps ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+				folder.add( material, 'combine', constants.combine ).onChange( updateCombine( material ) );
+				folder.add( material, 'reflectivity', 0, 1 );
+				folder.add( material, 'refractionRatio', 0, 1 );
+
+			}
+
+			function guiMeshToonMaterial( gui, mesh, material ) {
+
+				const data = {
+					color: material.color.getHex(),
+					map: diffuseMapKeys[ 0 ],
+					gradientMap: gradientMapKeys[ 1 ],
+					alphaMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshToonMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'gradientMap', gradientMapKeys ).onChange( updateTexture( material, 'gradientMap', gradientMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+
+			}
+
+			function guiMeshStandardMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					emissive: material.emissive.getHex(),
+					envMaps: envMapKeysPBR[ 0 ],
+					map: diffuseMapKeys[ 0 ],
+					roughnessMap: roughnessMapKeys[ 0 ],
+					alphaMap: alphaMapKeys[ 0 ],
+					metalnessMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshStandardMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.addColor( data, 'emissive' ).onChange( handleColorChange( material.emissive ) );
+
+				folder.add( material, 'roughness', 0, 1 );
+				folder.add( material, 'metalness', 0, 1 );
+				folder.add( material, 'flatShading' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( data, 'envMaps', envMapKeysPBR ).onChange( updateTexture( material, 'envMap', envMaps ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'roughnessMap', roughnessMapKeys ).onChange( updateTexture( material, 'roughnessMap', roughnessMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+				folder.add( data, 'metalnessMap', alphaMapKeys ).onChange( updateTexture( material, 'metalnessMap', alphaMaps ) );
+
+			}
+
+			function guiMeshPhysicalMaterial( gui, mesh, material, geometry ) {
+
+				const data = {
+					color: material.color.getHex(),
+					emissive: material.emissive.getHex(),
+					envMaps: envMapKeys[ 0 ],
+					map: diffuseMapKeys[ 0 ],
+					roughnessMap: roughnessMapKeys[ 0 ],
+					alphaMap: alphaMapKeys[ 0 ],
+					metalnessMap: alphaMapKeys[ 0 ],
+					sheenColor: material.sheenColor.getHex(),
+					specularColor: material.specularColor.getHex(),
+					iridescenceMap: alphaMapKeys[ 0 ]
+				};
+
+				const folder = gui.addFolder( 'THREE.MeshPhysicalMaterial' );
+
+				folder.addColor( data, 'color' ).onChange( handleColorChange( material.color ) );
+				folder.addColor( data, 'emissive' ).onChange( handleColorChange( material.emissive ) );
+
+				folder.add( material, 'roughness', 0, 1 );
+				folder.add( material, 'metalness', 0, 1 );
+				folder.add( material, 'ior', 1, 2.333 );
+				folder.add( material, 'reflectivity', 0, 1 );
+				folder.add( material, 'iridescence', 0, 1 );
+				folder.add( material, 'iridescenceIOR', 1, 2.333 );
+				folder.add( material, 'sheen', 0, 1 );
+				folder.add( material, 'sheenRoughness', 0, 1 );
+				folder.addColor( data, 'sheenColor' ).onChange( handleColorChange( material.sheenColor ) );
+				folder.add( material, 'clearcoat', 0, 1 ).step( 0.01 );
+				folder.add( material, 'clearcoatRoughness', 0, 1 ).step( 0.01 );
+				folder.add( material, 'specularIntensity', 0, 1 );
+				folder.addColor( data, 'specularColor' ).onChange( handleColorChange( material.specularColor ) );
+				folder.add( material, 'flatShading' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'wireframe' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'vertexColors' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( material, 'fog' ).onChange( needsUpdate( material, geometry ) );
+				folder.add( data, 'envMaps', envMapKeysPBR ).onChange( updateTexture( material, 'envMap', envMaps ) );
+				folder.add( data, 'map', diffuseMapKeys ).onChange( updateTexture( material, 'map', diffuseMaps ) );
+				folder.add( data, 'roughnessMap', roughnessMapKeys ).onChange( updateTexture( material, 'roughnessMap', roughnessMaps ) );
+				folder.add( data, 'alphaMap', alphaMapKeys ).onChange( updateTexture( material, 'alphaMap', alphaMaps ) );
+				folder.add( data, 'metalnessMap', alphaMapKeys ).onChange( updateTexture( material, 'metalnessMap', alphaMaps ) );
+				folder.add( data, 'iridescenceMap', alphaMapKeys ).onChange( updateTexture( material, 'iridescenceMap', alphaMaps ) );
+
+			}
+
+			function chooseFromHash( gui, mesh, geometry ) {
+
+				const selectedMaterial = window.location.hash.substring( 1 ) || 'MeshBasicMaterial';
+
+				let material;
+
+				switch ( selectedMaterial ) {
+
+					case 'MeshBasicMaterial' :
+
+						material = new THREE.MeshBasicMaterial( { color: 0x049EF4 } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshBasicMaterial( gui, mesh, material, geometry );
+
+						return material;
+
+						break;
+
+					case 'MeshLambertMaterial' :
+
+						material = new THREE.MeshLambertMaterial( { color: 0x049EF4 } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshLambertMaterial( gui, mesh, material, geometry );
+
+						return material;
+
+						break;
+
+					case 'MeshMatcapMaterial' :
+
+						material = new THREE.MeshMatcapMaterial( { matcap: matcaps.porcelainWhite } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshMatcapMaterial( gui, mesh, material, geometry );
+
+						// no need for lights
+
+						light1.visible = false;
+						light2.visible = false;
+						light3.visible = false;
+
+						return material;
+
+						break;
+
+					case 'MeshPhongMaterial' :
+
+						material = new THREE.MeshPhongMaterial( { color: 0x049EF4 } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshPhongMaterial( gui, mesh, material, geometry );
+
+						return material;
+
+						break;
+
+					case 'MeshToonMaterial' :
+
+						material = new THREE.MeshToonMaterial( { color: 0x049EF4, gradientMap: gradientMaps.threeTone } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshToonMaterial( gui, mesh, material );
+
+						// only use a single point light
+
+						light1.visible = false;
+						light3.visible = false;
+
+						return material;
+
+						break;
+
+					case 'MeshStandardMaterial' :
+
+						material = new THREE.MeshStandardMaterial( { color: 0x049EF4 } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshStandardMaterial( gui, mesh, material, geometry );
+
+						// only use scene environment
+
+						light1.visible = false;
+						light2.visible = false;
+						light3.visible = false;
+
+						return material;
+
+						break;
+
+					case 'MeshPhysicalMaterial' :
+
+						material = new THREE.MeshPhysicalMaterial( { color: 0x049EF4 } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshPhysicalMaterial( gui, mesh, material, geometry );
+
+						// only use scene environment
+
+						light1.visible = false;
+						light2.visible = false;
+						light3.visible = false;
+
+						return material;
+
+						break;
+
+					case 'MeshDepthMaterial' :
+
+						material = new THREE.MeshDepthMaterial();
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshDepthMaterial( gui, mesh, material );
+
+						return material;
+
+						break;
+
+					case 'MeshNormalMaterial' :
+
+						material = new THREE.MeshNormalMaterial();
+						guiMaterial( gui, mesh, material, geometry );
+						guiMeshNormalMaterial( gui, mesh, material, geometry );
+
+						return material;
+
+						break;
+
+					case 'LineBasicMaterial' :
+
+						material = new THREE.LineBasicMaterial( { color: 0x2194CE } );
+						guiMaterial( gui, mesh, material, geometry );
+						guiLineBasicMaterial( gui, mesh, material, geometry );
+
+						return material;
+
+						break;
+
+				}
+
+			}
+
+			//
+
+			const selectedMaterial = window.location.hash.substring( 1 );
+
+			if ( THREE[ selectedMaterial ] !== undefined ) {
+
+				document.getElementById( 'newWindow' ).href += '#' + selectedMaterial;
+
+			}
+
+			const gui = new GUI();
+
+			const renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			document.body.appendChild( renderer.domElement );
+
+			const pmremGenerator = new THREE.PMREMGenerator( renderer );
+
+			const scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x444444 );
+			scene.environment = pmremGenerator.fromScene( new RoomEnvironment(), 0.04 ).texture;
+
+			const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 10, 100 );
+			camera.position.z = 35;
+
+			const ambientLight = new THREE.AmbientLight( 0x000000 );
+			scene.add( ambientLight );
+
+			const light1 = new THREE.DirectionalLight( 0xffffff, 3 );
+			light1.position.set( 0, 200, 0 );
+			scene.add( light1 );
+
+			const light2 = new THREE.DirectionalLight( 0xffffff, 3 );
+			light2.position.set( 100, 200, 100 );
+			scene.add( light2 );
+
+			const light3 = new THREE.DirectionalLight( 0xffffff, 3 );
+			light3.position.set( - 100, - 200, - 100 );
+			scene.add( light3 );
+
+			guiScene( gui, scene );
+
+			const geometry = new THREE.TorusKnotGeometry( 10, 3, 200, 32 ).toNonIndexed();
+
+			generateVertexColors( geometry );
+
+			const mesh = new THREE.Mesh( geometry );
+			mesh.material = chooseFromHash( gui, mesh, geometry );
+
+			scene.add( mesh );
+
+			let prevFog = false;
+
+			function render() {
+
+				requestAnimationFrame( render );
+
+				mesh.rotation.x += 0.005;
+				mesh.rotation.y += 0.005;
+
+				if ( prevFog !== scene.fog ) {
+
+					mesh.material.needsUpdate = true;
+					prevFog = scene.fog;
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+			window.addEventListener( 'resize', function () {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}, false );
+
+			render();
+
+		</script>
+	</body>
+</html>

Разница между файлами не показана из-за своего большого размера
+ 0 - 8
utils/docs/template/static/scripts/fuse/fuse.js


+ 22 - 229
utils/docs/template/static/scripts/page.js

@@ -1,252 +1,45 @@
-( function handleLegacyURLs() {
+// Initialize prettify for syntax highlighting
+if ( typeof prettyPrint === 'function' ) {
 
-	const hash = window.location.hash;
-
-	if ( hash.startsWith( '#api/' ) || hash.startsWith( '#examples/' ) ) {
-
-		const mappings = {
-
-			'3DMLoader': 'Rhino3dmLoader',
-
-			'BufferGeometryUtils': 'module-BufferGeometryUtils',
-			'CameraUtils': 'module-CameraUtils',
-			'SceneUtils': 'module-SceneUtils',
-			'SkeletonUtils': 'module-SkeletonUtils',
-			'UniformsUtils': 'module-UniformsUtils',
-
-			'DefaultLoadingManager': 'LoadingManager',
-			'Interpolations': 'module-Interpolations',
-
-			'Animation': 'global',
-			'BufferAttributeUsage': 'global',
-			'Core': 'global',
-			'CustomBlendingEquations': 'global',
-			'Materials': 'global',
-			'Textures': 'global'
-		};
-
-		const parts = hash.split( '/' );
-		let className = parts[ parts.length - 1 ];
-
-		if ( className ) {
-
-			if ( className in mappings ) className = mappings[ className ];
-
-			window.location.href = `${className}.html`;
-
-		}
-
-	}
-
-} )();
-
-( function loadNavigation() {
-
-	const content = document.getElementById( 'content' );
-	const navContainer = content.querySelector( 'nav' );
-
-	fetch( 'nav.html' )
-		.then( response => response.text() )
-		.then( html => {
-
-			navContainer.innerHTML = html;
-
-			const savedScrollTop = sessionStorage.getItem( 'navScrollTop' );
-
-			if ( savedScrollTop !== null ) {
-
-				content.scrollTop = parseInt( savedScrollTop, 10 );
-
-			}
-
-			// Save scroll position when clicking nav links
-			navContainer.addEventListener( 'click', function ( event ) {
-
-				const link = event.target.closest( 'a' );
-
-				if ( link ) {
-
-					sessionStorage.setItem( 'navScrollTop', content.scrollTop );
-
-				}
-
-			} );
-
-			updateNavigation();
-
-			sessionStorage.removeItem( 'navScrollTop' );
-
-		} )
-		.catch( err => console.error( 'Failed to load navigation:', err ) );
-
-} )();
-
-//
-
-const panel = document.getElementById( 'panel' );
-const panelScrim = document.getElementById( 'panelScrim' );
-const expandButton = document.getElementById( 'expandButton' );
-const clearSearchButton = document.getElementById( 'clearSearchButton' );
-const filterInput = document.getElementById( 'filterInput' );
-
-// code copy buttons
-
-const elements = document.getElementsByTagName( 'pre' );
-
-for ( let i = 0; i < elements.length; i ++ ) {
-
-	const element = elements[ i ];
-
-	if ( element.classList.contains( 'linenums' ) === false ) {
-
-		addCopyButton( element );
-
-	}
+	prettyPrint();
 
 }
 
-function addCopyButton( element ) {
+// Add code copy buttons
+( function addCopyButtons() {
 
-	const copyButton = document.createElement( 'button' );
-	copyButton.className = 'copy-btn';
+	const elements = document.getElementsByTagName( 'pre' );
 
-	element.appendChild( copyButton );
+	for ( let i = 0; i < elements.length; i ++ ) {
 
-	copyButton.addEventListener( 'click', function () {
+		const element = elements[ i ];
 
-		const codeContent = element.textContent;
-		navigator.clipboard.writeText( codeContent ).then( () => {
+		if ( element.classList.contains( 'linenums' ) === false ) {
 
-			copyButton.classList.add( 'copied' );
+			const copyButton = document.createElement( 'button' );
+			copyButton.className = 'copy-btn';
 
-			setTimeout( () => {
+			element.appendChild( copyButton );
 
-				copyButton.classList.remove( 'copied' );
+			copyButton.addEventListener( 'click', function () {
 
-			}, 1000 );
+				const codeContent = element.textContent;
+				navigator.clipboard.writeText( codeContent ).then( () => {
 
-		} );
-
-	} );
-
-}
+					copyButton.classList.add( 'copied' );
 
-// Functionality for hamburger button (on small devices)
+					setTimeout( () => {
 
-expandButton.onclick = function ( event ) {
+						copyButton.classList.remove( 'copied' );
 
-	event.preventDefault();
-	panel.classList.toggle( 'open' );
+					}, 1000 );
 
-};
+				} );
 
-panelScrim.onclick = function ( event ) {
-
-	event.preventDefault();
-	panel.classList.toggle( 'open' );
-
-};
-
-// Functionality for search/filter input field
-
-filterInput.onfocus = function () {
-
-	panel.classList.add( 'searchFocused' );
-
-};
-
-filterInput.onblur = function () {
-
-	if ( filterInput.value === '' ) {
-
-		panel.classList.remove( 'searchFocused' );
-
-	}
-
-};
-
-filterInput.oninput = function () {
-
-	const term = filterInput.value.trim();
-
-	// eslint-disable-next-line no-undef
-	search( term ); // defined in search.js
-
-};
-
-clearSearchButton.onclick = function () {
-
-	filterInput.value = '';
-	filterInput.focus();
-	// eslint-disable-next-line no-undef
-	hideSearch(); // defined in search.js
-
-};
-
-//
-
-window.addEventListener( 'hashchange', updateNavigation );
-
-function updateNavigation() {
-
-	// unselected elements
-
-	const selected = document.querySelectorAll( 'nav a.selected' );
-	selected.forEach( link => link.classList.remove( 'selected' ) );
-
-	// determine target
-
-	const filename = window.location.pathname.split( '/' ).pop();
-	const pagename = filename.split( '.' )[ 0 ];
-
-	let target = pagename.replace( 'module-', '' );
-
-	if ( pagename === 'global' ) {
-
-		target = window.location.hash.split( '#' ).pop();
-
-	}
-
-	if ( target === '' ) return;
-
-	// select target and move into view
-
-	const aElement = document.querySelector( `nav a[href="${filename}"], nav a[href="${filename}#${target}"]` );
-
-	if ( aElement !== null ) {
-
-		const savedScrollTop = sessionStorage.getItem( 'navScrollTop' );
-
-		if ( savedScrollTop === null ) {
-
-			aElement.scrollIntoView( { block: 'center' } );
+			} );
 
 		}
 
-		aElement.classList.add( 'selected' );
-
 	}
 
-}
-
-// eslint-disable-next-line no-undef
-prettyPrint();
-
-console.log( [
-	'    __     __',
-	' __/ __\\  / __\\__   ____   _____   _____',
-	'/ __/  /\\/ /  /___\\/ ____\\/ _____\\/ _____\\',
-	'\\/_   __/ /   _   / /  __/ / __  / / __  /_   __   _____',
-	'/ /  / / /  / /  / /  / / /  ___/ /  ___/\\ _\\/ __\\/ _____\\',
-	'\\/__/  \\/__/\\/__/\\/__/  \\/_____/\\/_____/\\/__/ /  / /  ___/',
-	'                                         / __/  /  \\__  \\',
-	'                                         \\/____/\\/_____/'
-].join( '\n' ) );
-
-// console sandbox
-
-import( '/build/three.module.js' ).then( THREE => {
-
-	window.THREE = THREE;
-
-} );
+} )();

+ 0 - 159
utils/docs/template/static/scripts/search.js

@@ -1,159 +0,0 @@
-const contentContainer = document.querySelector( '#page-content' );
-const searchContainer = document.querySelector( '#search-content' );
-const resultBox = document.querySelector( '#search-result' );
-
-// eslint-disable-next-line no-unused-vars
-function hideSearch() {
-
-	searchContainer.style.display = 'none';
-	contentContainer.style.display = 'block';
-
-
-}
-
-function showResultText( text ) {
-
-	resultBox.innerHTML = text;
-
-}
-
-function showSearch() {
-
-	searchContainer.style.display = 'block';
-	contentContainer.style.display = 'none';
-
-}
-
-function extractUrlBase( url ) {
-
-	const index = url.lastIndexOf( '/' );
-
-	if ( index === - 1 ) return './';
-
-	return url.slice( 0, index + 1 );
-
-}
-
-async function fetchAllData() {
-
-	const { origin, pathname } = location;
-
-	const baseURL = extractUrlBase( pathname );
-
-	const base = origin + baseURL;
-
-	const url = new URL( 'data/search.json', base );
-	const result = await fetch( url );
-	const { list } = await result.json();
-
-	return list;
-
-}
-
-
-function buildSearchResult( result ) {
-
-	let output = '';
-	const removeHTMLTagsRegExp = /(<([^>]+)>)/ig;
-
-	for ( const res of result ) {
-
-		const { title = '', description = '' } = res.item;
-
-		const _link = res.item.link.replace( '<a href="', '' ).replace( /">.*/, '' );
-		const _title = title.replace( removeHTMLTagsRegExp, '' );
-		const _description = description.replace( removeHTMLTagsRegExp, '' );
-
-		output += `
-    <a href="${_link}" class="search-result-item" onclick="hideSearch()">
-      <span class="search-result-item-title">${_title}</span>
-      <span class="search-result-item-description">- ${_description || 'No description available.'}</span>
-    </a>
-    `;
-
-	}
-
-	return output;
-
-}
-
-function getSearchResult( list, keys, searchKey ) {
-
-	const defaultOptions = {
-		shouldSort: true,
-		threshold: 0.4,
-		location: 0,
-		distance: 100,
-		maxPatternLength: 32,
-		minMatchCharLength: 1,
-		keys: keys
-	};
-
-	const options = { ...defaultOptions };
-
-	// eslint-disable-next-line no-undef
-	const searchIndex = Fuse.createIndex( options.keys, list );
-
-	// eslint-disable-next-line no-undef
-	const fuse = new Fuse( list, options, searchIndex );
-
-	const result = fuse.search( searchKey );
-
-	if ( result.length > 20 ) {
-
-		return result.slice( 0, 20 );
-
-	}
-
-	return result;
-
-}
-
-let searchData;
-
-// eslint-disable-next-line no-unused-vars
-async function search( value ) {
-
-	if ( value === '' ) {
-
-		hideSearch();
-		return;
-
-	}
-
-	showSearch();
-
-	const keys = [ 'title', 'description' ];
-
-	if ( searchData === undefined ) {
-
-		showResultText( 'Loading...' );
-
-		try {
-
-			searchData = await fetchAllData();
-
-		} catch ( e ) {
-
-			console.log( e );
-			showResultText( 'Failed to load result.' );
-
-			return;
-
-		}
-
-	}
-
-	const result = getSearchResult( searchData, keys, value );
-
-	if ( ! result.length ) {
-
-		showResultText( 'No result found! Try some different combination.' );
-
-		return;
-
-	}
-
-	resultBox.innerHTML = buildSearchResult( result );
-
-}

+ 302 - 270
utils/docs/template/static/styles/page.css

@@ -1,3 +1,29 @@
+:root {
+	color-scheme: light dark;
+
+	--color-blue: #049EF4;
+	--text-color: #444;
+
+	--font-size: 16px;
+	--line-height: 26px;
+
+	--border-style: 1px solid #E8E8E8;
+	--panel-width: 300px;
+	--page-padding: 24px;
+	--max-width: 760px;
+	--icon-size: 20px;
+}
+
+@media (prefers-color-scheme: dark) {
+
+	:root {
+		--text-color: #bbb;
+
+		--border-style: 1px solid #444;
+	}
+
+}
+
 @font-face {
 	font-family: 'Roboto Mono';
 	src: local('Roboto Mono'), local('RobotoMono-Regular'), url('/files/RobotoMono-Regular.woff2') format('woff2');
@@ -19,321 +45,319 @@
 	src: local('Inter-SemiBold'), url("/files/Inter-SemiBold.woff2?v=3.6") format("woff2");
 }
 
-/* Below CSS should only affect the page section */
+html {
+	font-family: 'Inter', sans-serif;
+	font-size: var(--font-size);
+	line-height: var(--line-height);
+}
 
-#page {
-	--font-size: 16px;
-	--line-height: 26px;
-	--panel-width: 300px;
-	--page-padding: 24px;
-	--max-width: 960px;
-	--icon-size: 20px;
-	--border-style: 1px solid #E8E8E8;
-	--text-color: #444;
+body {
+	margin: 0;
+	padding: 0;
+}
 
-	font-family: 'Inter', sans-serif;
+#page {
 	color: var(--text-color);
 	tab-size: 4;
 	overflow: auto;
-	max-width: 760px;
+	max-width: var(--max-width);
 	margin: 0 auto;
 	padding-top: var(--page-padding);
 	padding-bottom: var(--page-padding);
 	padding-right: var(--page-padding);
 	padding-left: var(--page-padding);
 	word-break: break-word;
+}
 
-	a {
-		color: var(--color-blue);
-		cursor: pointer;
-		text-decoration: none;
-	}
-	
-	h1 {
-		font-size: 40px;
-		line-height: 48px;
-		font-weight: normal;
-		margin-left: -2px;
-		margin-top: 16px;
-		margin-bottom: -8px;
-	}
+#page a {
+	color: var(--color-blue);
+	cursor: pointer;
+	text-decoration: none;
+}
 
-	h2 {
-		font-size: 28px;
-		line-height: 36px;
-		font-weight: normal;
-		margin-left: -1px;
-		margin-top: 28px;
-		margin-bottom: -8px;
-		color: #000;
-	}
+#page h1 {
+	font-size: 40px;
+	line-height: 48px;
+	font-weight: normal;
+	margin-left: -2px;
+	margin-top: 16px;
+	margin-bottom: -8px;
+}
 
-	h3 {
-		font-size: 20px;
-		line-height: 28px;
-		font-weight: normal;
-		margin-top: 24px;
-		margin-bottom: -8px;
-	}
+#page h2 {
+	font-size: 28px;
+	line-height: 36px;
+	font-weight: normal;
+	margin-left: -1px;
+	margin-top: 28px;
+	margin-bottom: -8px;
+}
 
-	p,
-	div,
-	table,
-	ol,
-	ul {
-		margin-top: 16px;
-		margin-bottom: 16px;
-	}
-	
-	p {
-		padding-right: 16px;
-	}
-	
-	ul, ol {
-		box-sizing: border-box;
-		padding-left: 24px;
-	}
-	ul li,
-	ol li {
-		padding-left: 4px;
-		margin-bottom: 4px;
-	}
-	
-	li ul,
-	li ol {
-		margin-top: 4px;
-	}
+#page h3 {
+	font-size: 20px;
+	line-height: 28px;
+	font-weight: normal;
+	margin-top: 24px;
+	margin-bottom: -8px;
+}
 
-	table {
-		width: calc(100% - 40px);
-		border-collapse: collapse;
-		margin-left: 20px;
-		margin-right: 20px;
-	}
-	
-	.desc {
-		padding-left: 0px;
-	}
-	
-	table th,
-	table td {
-		text-align: left;
-		vertical-align: top;
-		padding: 8px 6px;
-	}
+#page p,
+#page div,
+#page table,
+#page ol,
+#page ul,
+#page summary {
+	margin-top: 16px;
+	margin-bottom: 16px;
+}
 
-	table td.name,
-	table th.name {
-		white-space: nowrap;
-		width: 150px;
-	}
-	
-	table th {
-		text-decoration: none;
-	}
-	table th:first-child,
-	table td:first-child {
-		padding-left: 0;
-	}
+#page summary:hover {
+	cursor: pointer;
+}
 
-	table p {
-		margin: 0;
-	}
-	
-	strong {
-		font-weight: 600;
-	}
-	
-	a.permalink {
-		float: right;
-		margin-left: 5px;
-		display: none;
-	}
-	
-	a.param,
-	span.param {
-		color: #999;
-	}
-	
-	a.param:hover {
-		color: var(--text-color);
-	}
+#page p {
+	padding-right: 16px;
+}
 
-	.inheritance {
-		color: #999;
-		margin-bottom: 0;
-	}
+#page ul, #page ol {
+	box-sizing: border-box;
+	padding-left: 24px;
+}
+#page ul li,
+#page ol li {
+	padding-left: 4px;
+	margin-bottom: 4px;
+}
 
-	.method,
-	.member {
-		margin-bottom: 32px;
-	}
+#page li ul,
+#page li ol {
+	margin-top: 4px;
+}
 
-	ol.linenums {
-		padding-left: 64px;
-	}
+#page code {
+	display: inline-block;
+	vertical-align: middle;
+	border-radius: 4px;
+	padding: 0px 5px;
+	background: #F5F5F5;
+}
 
-	ol.linenums .selected {
-		background-color: #ddd;
-	}
+#page pre {
+	position: relative;
+	margin: 16px calc(-1 * var(--page-padding));
+}
 
-	code {
-		display: inline-block;
-		vertical-align: middle;
-		border-radius: 4px;
-		padding: 0px 5px;
-		background-color: #F5F5F5;
-	}
+#page pre code {
+	display: block;
+	font-size: calc(var(--font-size) - 1px);
+	line-height: calc(var(--line-height) - 1px);
+	padding: calc(var(--page-padding) - 6px) var(--page-padding);
+	white-space: pre-wrap;
+	overflow: auto;
+	box-sizing: border-box;
+	background: #F5F5F5;
+	border-radius: 0;
+}
 
-	pre {
-		overflow: auto;
-		white-space: pre-wrap;
-		font-size: calc(var(--font-size) - 1px);
-		line-height: calc(var(--line-height) - 1px);
-		position: relative;
-	}
+#page ol code,
+#page ul code {
+	margin: 0;
+}
 
-	pre code {
-		background-color: inherit;
-		padding: calc(var(--page-padding) - 6px) var(--page-padding);
-	}
+#page .copy-btn {
+	cursor: pointer;
+	position: absolute;
+	top: 16px;
+	right: 16px;
+	width: 24px;
+	height: 24px;
+	background-color: transparent;
+	background-image: url('/files/ic_copy_grey_24dp.svg');
+	background-size: contain;
+	background-position: center;
+	background-repeat: no-repeat;
+	opacity: 0.9;
+	border: none;
+}
 
-	pre.linenums code {
-		padding: 0px 5px;
-	}
+#page .copy-btn:hover {
+	opacity: 1;
+}
 
-	h3.name {
-		color: #000;
-	}
+#page .copy-btn.copied {
+	pointer-events: none;
+	opacity: 1;
+	background-image: url('/files/ic_tick_green_24dp.svg');
+}
 
-	h3.name a {
-		color: var(--color-blue);
-		text-decoration: none;
-	}
+#page table {
+	width: 100%;
+	border-collapse: collapse;
+}
 
-	h3.name .signature {
-		color: #000;
-		font-weight: normal;
-	}
+#page .desc {
+	padding-left: 0px;
+}
 
-	h3.name .type-signature {
-		color: #999;
-		font-weight: normal;
-	}
+#page table th,
+#page table td {
+	text-align: left;
+	vertical-align: top;
+	padding: 8px 6px;
+	border-bottom: var(--border-style);
+}
 
-	h3.name .type-signature a {
-		color: #999;
-	}
+#page table th {
+	text-decoration: none;
+}
+#page table th:first-child,
+#page table td:first-child {
+	padding-left: 0;
+}
 
-	h3.name .param-type {
-		color: #999;
-	}
+#page table code {
+	padding: 0px;
+	margin: 0px;
+	width: auto;
+}
 
-	h3.name .param-type a {
-		color: #999;
-	}
+#page table p {
+	margin: 0;
+}
 
-	.search-result-item {
-		border-bottom: var(--border-style);
-		display: block;
-		padding: 16px 0;
-	}
+#page strong {
+	font-weight: 600;
+}
 
-	.search-result-item-description {
-		color: var(--text-color);
-	}
+#page a.param,
+#page span.param {
+	color: #999;
+}
 
-	h2.subsection-title + p {
-		margin-top: 24px;
-	}
+#page a.param:hover {
+	color: var(--text-color);
+}
 
-	.copy-btn {
-		cursor: pointer;
-		position: absolute;
-		top: 16px;
-		right: 16px;
-		width: 24px;
-		height: 24px;
-		background-color: transparent;
-		background-image: url('/files/ic_copy_grey_24dp.svg');
-		background-size: contain;
-		background-position: center;
-		background-repeat: no-repeat;
-		opacity: 0.9;
-		border: none;
-	}
-	
-	.copy-btn:hover {
-		opacity: 1;
-	}
-	
-	.copy-btn.copied {
-		pointer-events: none;
-		opacity: 1;
-		background-image: url('/files/ic_tick_green_24dp.svg');
-	}
+#page .inheritance {
+	color: #999;
+	margin-bottom: 0;
+	font-size: var(--font-size);
+	line-height: var(--line-height);
+	margin-top: 0;
+}
 
+#page .method,
+#page .member {
+	margin-bottom: 32px;
 }
 
-@media (prefers-color-scheme: dark) {
+#page ol.linenums {
+	padding-left: 64px;
+}
 
-	#page {
-		--text-color: #bbb;
-		--border-style: 1px solid #444;
+#page ol.linenums .selected {
+	background-color: #ddd;
+}
+
+#page h3.name {
+	color: #000;
+}
+
+#page h3.name a {
+	color: var(--color-blue);
+	text-decoration: none;
+}
+
+#page h3.name .signature {
+	color: #000;
+	font-weight: normal;
+}
+
+#page h3.name .type-signature {
+	color: #999;
+	font-weight: normal;
+}
 
-		h2 {
-			color: var(--text-color);
-		}
+#page h3.name .type-signature a {
+	color: #999;
+}
+
+#page h3.name .param-type {
+	color: #999;
+}
 
-		h3.name {
-			color: var(--text-color);
-		}
+#page h3.name .param-type a {
+	color: #999;
+}
+
+#page h2.subsection-title + p {
+	margin-top: 24px;
+}
+
+#page iframe#viewer {
+	width: 100%;
+	height: 400px;
+	border: none;
+	margin-top: 16px;
+	margin-bottom: 16px;
+}
 
-		h3.name .signature {
-			color: var(--text-color);
-		}
+@media (prefers-color-scheme: dark) {
+
+	#page h2 {
+		color: var(--text-color);
+	}
+
+	#page h3.name {
+		color: var(--text-color);
+	}
+
+	#page h3.name .signature {
+		color: var(--text-color);
+	}
 
-		.link-anchor {
-			color: #555;
-		}
+	#page .link-anchor {
+		color: #555;
+	}
 
-		ol.linenums .selected {
-			background-color: #444;
-		}
+	#page ol.linenums .selected {
+		background-color: #444;
+	}
 
-		code {
-			background-color: #444;
-		}
+	#page code {
+		background: #444;
+	}
 
+	#page pre code {
+		background: #444;
 	}
 
 }
 
 @media all and ( min-width: 1700px ) {
 
-	#page {
+	:root {
 		--panel-width: 360px;
 		--font-size: 18px;
 		--line-height: 28px;
+		--max-width: 880px;
 		--page-padding: 28px;
 		--icon-size: 24px;
-		max-width: 880px;
-
-		h1 {
-			font-size: 42px;
-			line-height: 50px;
-		}
+	}
 
-		h2 {
-			font-size: 32px;
-			line-height: 40px;
-		}
+	#page h1 {
+		font-size: 42px;
+		line-height: 50px;
+	}
 
-		h3 {
-			font-size: 24px;
-			line-height: 32px;
-		}
+	#page h2 {
+		font-size: 32px;
+		line-height: 40px;
+	}
 
+	#page h3 {
+		font-size: 24px;
+		line-height: 32px;
 	}
 
 }
@@ -342,29 +366,37 @@
 
 @media all and ( max-width: 640px ) {
 
-	#page {
+	:root {
 		--page-padding: 16px;
 		--icon-size: 24px;
+	}
 
+	#page {
 		padding: var(--page-padding);
+	}
 
-		h1 {
-			font-size: 28px;
-			line-height: 36px;
-			padding-right: 20px;
-			margin-top: 0;
-		}
+	#page h1 {
+		font-size: 28px;
+		line-height: 36px;
+		padding-right: 20px;
+		margin-top: 0;
+	}
 
-		h2 {
-			font-size: 24px;
-			line-height: 32px;
-			margin-top: 24px;
-		}
+	#page h2 {
+		font-size: 24px;
+		line-height: 32px;
+		margin-top: 24px;
+	}
 
-		h3 {
-			font-size: 20px;
-			line-height: 28px;
-		}
+	#page h3 {
+		font-size: 20px;
+		line-height: 28px;
+	}
 
+	#page .copy-btn {
+		top: 12px;
+		right: 8px;
+		width: 20px;
+		height: 20px;
 	}
-}
+}

+ 153 - 150
utils/docs/template/tmpl/container.tmpl

@@ -1,163 +1,166 @@
 <?js
 	var self = this;
 	var isGlobalPage;
+	var isTSLPage;
 
 	docs.forEach(function(doc, i) {
 ?>
 
 <?js
-	// we only need to check this once
-	if (typeof isGlobalPage === 'undefined') {
-		isGlobalPage = (doc.kind === 'globalobj');
-	}
-?>
-<?js if (doc.kind === 'mainpage' || (doc.kind === 'package')) { ?>
-	<?js= self.partial('mainpage.tmpl', doc) ?>
-<?js } else if (doc.kind === 'source') { ?>
-	<?js= self.partial('source.tmpl', doc) ?>
-<?js } else { ?>
-
-<section>
-
-<header>
-	<?js if (!doc.longname || doc.kind !== 'module') { ?>
-		<?js if (doc.classdesc) { ?>
-			<div class="class-description"><?js= doc.classdesc ?></div>
-		<?js } ?>
-	<?js } else if (doc.kind === 'module' && doc.modules) { ?>
-		<?js doc.modules.forEach(function(module) { ?>
-			<?js if (module.classdesc) { ?>
-				<div class="class-description"><?js= module.classdesc ?></div>
-			<?js } ?>
-		<?js }) ?>
-	<?js } ?>
-</header>
-
-<article>
-	<div class="container-overview">
-	<?js if (doc.kind === 'module' && doc.modules) { ?>
-		<?js if (doc.description) { ?>
-			<div class="description"><?js= doc.description ?></div>
-		<?js } ?>
-
-		<?js doc.modules.forEach(function(module) { ?>
-			<?js= self.partial('method.tmpl', module) ?>
-		<?js }) ?>
-	<?js } else if (doc.kind === 'class' || (doc.kind === 'namespace' && doc.signature)) { ?>
-		<?js= self.partial('method.tmpl', doc) ?>
-	<?js } else { ?>
-		<?js if (doc.description) { ?>
-			<div class="description"><?js= doc.description ?></div>
-		<?js } ?>
-
-		<?js= self.partial('details.tmpl', doc) ?>
-
-		<?js if (doc.examples && doc.examples.length) { ?>
-			<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
-			<?js= self.partial('examples.tmpl', doc.examples) ?>
-		<?js } ?>
-	<?js } ?>
-	</div>
-
-	<?js if (doc.import) { ?>
-		<h2 class="subsection-title">Import</h2>
-		<p><?js= doc.name ?> is an addon, and must be imported explicitly, see <a href="https://threejs.org/manual/#en/installation" target="_blank">Installation#Addons</a>.</p>
-		<pre class="prettyprint source lang-js"><code><?js= doc.import ?></code></pre>
-	<?js } ?>
-
-	<?js
-		var ignoreInheritedSymbols = { ...(self.ignoreInheritedSymbols ? { inherited: {isUndefined: true} } : {}) };
-		var classes = self.find({kind: 'class', memberof: doc.longname});
-		if (!isGlobalPage && classes && classes.length) {
-	?>
-		<h2 class="subsection-title">Classes</h2>
-
-		<dl><?js classes.forEach(function(c) { ?>
-			<dt><?js= self.linkto(c.longname, c.name) ?></dt>
-			<dd><?js if (c.summary) { ?><?js= c.summary ?><?js } ?></dd>
-		<?js }); ?></dl>
-	<?js } ?>
-
-	<?js
-		var members = self.find({kind: 'member', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
-
-		// symbols that are assigned to module.exports are not globals, even though they're not a memberof anything
-		if (isGlobalPage && members && members.length && members.forEach) {
-			members = members.filter(function(m) {
-				return m.longname && m.longname.indexOf('module:') !== 0;
-			});
+		// we only need to check this once
+		if (typeof isGlobalPage === 'undefined') {
+			isGlobalPage = (doc.kind === 'globalobj');
+			isTSLPage = (doc.isTSL === true);
 		}
-		if (members && members.length && members.forEach) {
 	?>
-		<h2 class="subsection-title">Properties</h2>
-
-		<?js members.forEach(function(p) { ?>
-			<?js= self.partial('members.tmpl', p) ?>
-		<?js }); ?>
-	<?js } ?>
-
-	<?js
-		var methods = self.find({kind: 'function', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
-		var staticMethods = methods.filter(function(m) { return m.scope === 'static'; });
-		var instanceMethods = methods.filter(function(m) { return m.scope !== 'static'; });
-
-		if (instanceMethods.length) {
-	?>
-		<h2 class="subsection-title">Methods</h2>
-
-		<?js instanceMethods.forEach(function(m) { ?>
-			<?js= self.partial('method.tmpl', m) ?>
-		<?js }); ?>
-	<?js } ?>
-
-	<?js if (staticMethods.length) { ?>
-		<h2 class="subsection-title">Static Methods</h2>
-
-		<?js staticMethods.forEach(function(m) { ?>
-			<?js= self.partial('method.tmpl', m) ?>
-		<?js }); ?>
-	<?js } ?>
-
-	<?js
-		var typedefs = self.find({kind: 'typedef', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
-		if (typedefs && typedefs.length && typedefs.forEach) {
-	?>
-		<h2 class="subsection-title">Type Definitions</h2>
-
-		<?js typedefs.forEach(function(e) {
-				if (e.signature) {
-			?>
-				<?js= self.partial('method.tmpl', e) ?>
-			<?js
-				}
-				else {
-			?>
-				<?js= self.partial('members.tmpl', e) ?>
-			<?js
-				}
-			}); ?>
-	<?js } ?>
-
-	<?js
-		var events = self.find({kind: 'event', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
-		if (events && events.length && events.forEach) {
-	?>
-		<h2 class="subsection-title">Events</h2>
-
-		<?js events.forEach(function(e) { ?>
-			<?js= self.partial('method.tmpl', e) ?>
-		<?js }); ?>
-	<?js } ?>
-
-	<?js if (doc.meta && doc.meta.shortpath) { ?>
-		<h2 class="subsection-title">Source</h2>
-		<p>
-			<a href="https://github.com/mrdoob/three.js/blob/master/<?js= doc.meta.shortpath ?>" target="_blank" rel="noopener"><?js= doc.meta.shortpath ?></a>
-		</p>
+	<?js if (doc.kind === 'mainpage' || (doc.kind === 'package')) { ?>
+		<?js= self.partial('mainpage.tmpl', doc) ?>
+	<?js } else if (doc.kind === 'source') { ?>
+		<?js= self.partial('source.tmpl', doc) ?>
+	<?js } else { ?>
+		<section>
+			<header>
+				<?js if (!doc.longname || doc.kind !== 'module') { ?>
+					<?js if (doc.classdesc) { ?>
+				<div class="class-description"><?js= doc.classdesc ?></div>
+					<?js } ?>
+				<?js } else if (doc.kind === 'module' && doc.modules) { ?>
+					<?js doc.modules.forEach(function(module) { ?>
+						<?js if (module.classdesc) { ?>
+				<div class="class-description"><?js= module.classdesc ?></div>
+						<?js } ?>
+					<?js }) ?>
+				<?js } ?>
+				<?js if (doc.demo) { ?>
+				<iframe id="viewer" src="../<?js= doc.demo ?>"></iframe>
+				<?js } ?>
+				<?js if (doc.codeExample) { ?>
+				<h2>Code Example</h2>
+				<?js= doc.codeExample ?>
+				<?js } ?>
+			</header>
+			<article>
+				<?js if (doc.import) { ?>
+				<h2 class="subsection-title">Import</h2>
+				<p><?js= doc.name ?> is an addon, and must be imported explicitly, see <a href="https://threejs.org/manual/#en/installation" target="_blank">Installation#Addons</a>.</p>
+				<pre class="prettyprint source lang-js"><code><?js= doc.import ?></code></pre>
+				<?js } ?>
+				<div class="container-overview">
+					<?js if (doc.kind === 'module' && doc.modules) { ?>
+						<?js if (doc.description) { ?>
+					<div class="description"><?js= doc.description ?></div>
+						<?js } ?>
+						<?js doc.modules.forEach(function(module) { ?>
+							<?js= self.partial('method.tmpl', module) ?>
+						<?js }) ?>
+					<?js } else if (doc.kind === 'class' || (doc.kind === 'namespace' && doc.signature)) { ?>
+						<?js= self.partial('method.tmpl', doc) ?>
+					<?js } else { ?>
+						<?js if (doc.description) { ?>
+					<div class="description"><?js= doc.description ?></div>
+						<?js } ?>
+						<?js= self.partial('details.tmpl', doc) ?>
+						<?js if (doc.examples && doc.examples.length) { ?>
+					<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
+							<?js= self.partial('examples.tmpl', doc.examples) ?>
+						<?js } ?>
+					<?js } ?>
+				</div>
+				<?js
+					var ignoreInheritedSymbols = { ...(self.ignoreInheritedSymbols ? { inherited: {isUndefined: true} } : {}) };
+					var classes = self.find({kind: 'class', memberof: doc.longname});
+					if (!isGlobalPage && classes && classes.length) {
+				?>
+				<h2 class="subsection-title">Classes</h2>
+				<dl>
+					<?js classes.forEach(function(c) { ?>
+					<dt><?js= self.linkto(c.longname, c.name) ?></dt>
+					<dd><?js if (c.summary) { ?><?js= c.summary ?><?js } ?></dd>
+					<?js }); ?>
+				</dl>
+				<?js } ?>
+				<?js
+					var members = self.find({kind: 'member', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
+
+					// symbols that are assigned to module.exports are not globals, even though they're not a memberof anything
+					if (isGlobalPage && members && members.length && members.forEach) {
+						members = members.filter(function(m) {
+							if (m.longname && m.longname.indexOf('module:') === 0) return false;
+
+							// Filter based on whether this is TSL or Global page
+							var hasTslTag = m.tags && m.tags.some(function(tag) { return tag.title === 'tsl'; });
+							return isTSLPage ? hasTslTag : !hasTslTag;
+						});
+					}
+					if (members && members.length && members.forEach) {
+				?>
+				<h2 class="subsection-title">Properties</h2>
+				<?js members.forEach(function(p) { ?>
+					<?js= self.partial('members.tmpl', p) ?>
+				<?js }); ?>
+				<?js } ?>
+				<?js
+					var methods = self.find({kind: 'function', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
+
+					// Filter methods for TSL vs Global
+					if (isGlobalPage && methods && methods.length) {
+						methods = methods.filter(function(m) {
+							var hasTslTag = m.tags && m.tags.some(function(tag) { return tag.title === 'tsl'; });
+							return isTSLPage ? hasTslTag : !hasTslTag;
+						});
+					}
+
+					var staticMethods = methods.filter(function(m) { return m.scope === 'static'; });
+					var instanceMethods = methods.filter(function(m) { return m.scope !== 'static'; });
+
+					if (instanceMethods.length) {
+				?>
+				<h2 class="subsection-title">Methods</h2>
+				<?js instanceMethods.forEach(function(m) { ?>
+					<?js= self.partial('method.tmpl', m) ?>
+				<?js }); ?>
+				<?js } ?>
+				<?js if (staticMethods.length) { ?>
+				<h2 class="subsection-title">Static Methods</h2>
+				<?js staticMethods.forEach(function(m) { ?>
+					<?js= self.partial('method.tmpl', m) ?>
+				<?js }); ?>
+				<?js } ?>
+				<?js
+					var typedefs = self.find({kind: 'typedef', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
+					if (typedefs && typedefs.length && typedefs.forEach) {
+				?>
+				<h2 class="subsection-title">Type Definitions</h2>
+				<?js typedefs.forEach(function(e) {
+						if (e.signature) {
+					?>
+					<?js= self.partial('method.tmpl', e) ?>
+					<?js
+						}
+						else {
+					?>
+					<?js= self.partial('members.tmpl', e) ?>
+					<?js
+						}
+					}); ?>
+				<?js } ?>
+				<?js
+					var events = self.find({kind: 'event', memberof: isGlobalPage ? {isUndefined: true} : doc.longname, ...ignoreInheritedSymbols});
+					if (events && events.length && events.forEach) {
+				?>
+				<h2 class="subsection-title">Events</h2>
+				<?js events.forEach(function(e) { ?>
+					<?js= self.partial('method.tmpl', e) ?>
+				<?js }); ?>
+				<?js } ?>
+				<?js if (doc.meta && doc.meta.shortpath) { ?>
+				<h2 class="subsection-title">Source</h2>
+				<p>
+					<a href="https://github.com/mrdoob/three.js/blob/master/<?js= doc.meta.shortpath ?>" target="_blank" rel="noopener"><?js= doc.meta.shortpath ?></a>
+				</p>
+				<?js } ?>
+			</article>
+		</section>
 	<?js } ?>
-</article>
-
-</section>
-<?js } ?>
 
 <?js }); ?>

+ 38 - 41
utils/docs/template/tmpl/details.tmpl

@@ -1,44 +1,41 @@
 <?js
-var data = obj;
-var self = this;
-var defaultObjectClass = '';
-
-// Check if the default value is an object or array; if so, apply code highlighting
-if (data.defaultvalue && (data.defaultvaluetype === 'object' || data.defaultvaluetype === 'array')) {
-	data.defaultvalue = "<pre class=\"prettyprint\"><code>" + data.defaultvalue + "</code></pre>";
-	defaultObjectClass = ' class="object-value"';
-}
-?>
-<?js
-	var properties = data.properties;
-	if (properties && properties.length && properties.forEach && !data.hideconstructor) {
+	var data = obj;
+	var self = this;
+	var defaultObjectClass = '';
+
+	// Check if the default value is an object or array; if so, apply code highlighting
+	if (data.defaultvalue && (data.defaultvaluetype === 'object' || data.defaultvaluetype === 'array')) {
+		data.defaultvalue = "<pre class=\"prettyprint\"><code>" + data.defaultvalue + "</code></pre>";
+		defaultObjectClass = ' class="object-value"';
+	}
 ?>
-
-	<h5 class="subsection-title">Properties:</h5>
-
-	<?js= this.partial('properties.tmpl', data) ?>
-
-<?js } ?>
-
-<dl class="details">
-
-	<?js if (data.overrides) { ?>
-	<dt class="tag-overrides"><strong>Overrides:</strong> <?js= this.linkto(data.overrides, this.htmlsafe(data.overrides)) ?></dt>
-	<?js } ?>
-
-	<?js if (data.deprecated) { ?>
-		<dt class="important tag-deprecated"><strong>Deprecated:</strong> <?js
-			if (data.deprecated === true) { ?>Yes<?js }
-			else { ?><?js= data.deprecated ?><?js }
-		?></dt>
-	<?js } ?>
-
-	<?js if (data.see && see.length) {?>
-	<dt class="tag-see">See:</dt>
-	<dd class="tag-see">
-		<ul><?js see.forEach(function(s) { ?>
-			<li><?js= self.linkto(s) ?></li>
-		<?js }); ?></ul>
-	</dd>
+	<?js
+		var properties = data.properties;
+		if (properties && properties.length && properties.forEach && !data.hideconstructor) {
+	?>
+						<h5 class="subsection-title">Properties:</h5>
+						<?js= this.partial('properties.tmpl', data) ?>
 	<?js } ?>
-</dl>
+	<?js if (data.overrides || data.deprecated || (data.see && see.length)) { ?>
+						<dl class="details">
+							<?js if (data.overrides) { ?>
+							<dt class="tag-overrides"><strong>Overrides:</strong> <?js= this.linkto(data.overrides, this.htmlsafe(data.overrides)) ?></dt>
+							<?js } ?>
+							<?js if (data.deprecated) { ?>
+							<dt class="important tag-deprecated"><strong>Deprecated:</strong> <?js
+								if (data.deprecated === true) { ?>Yes<?js }
+								else { ?><?js= data.deprecated ?><?js }
+							?></dt>
+							<?js } ?>
+							<?js if (data.see && see.length) {?>
+							<dt class="tag-see">See:</dt>
+							<dd class="tag-see">
+								<ul>
+									<?js see.forEach(function(s) { ?>
+									<li><?js= self.linkto(s) ?></li>
+									<?js }); ?>
+								</ul>
+							</dd>
+							<?js } ?>
+						</dl>
+	<?js } ?>

+ 14 - 51
utils/docs/template/tmpl/layout.tmpl

@@ -4,64 +4,27 @@
 	<meta charset="utf-8">
 	<title><?js= title || 'Home' ?> - Three.js Docs</title>
 	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-	<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)"/>
-	<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)" />
-	<link rel="stylesheet" type="text/css" href="/files/main.css">
 
-	<script src="scripts/fuse/fuse.js"></script>
-	<script src="scripts/prettify/prettify.js"></script>
-	<script src="scripts/prettify/lang-css.js"></script>
+	<script src="../scripts/prettify/prettify.js"></script>
+	<script src="../scripts/prettify/lang-css.js"></script>
 
-	<link type="text/css" rel="stylesheet" href="styles/prettify-three.css">
-	<link type="text/css" rel="stylesheet" href="styles/page.css">
+	<link type="text/css" rel="stylesheet" href="../styles/prettify-three.css">
+	<link type="text/css" rel="stylesheet" href="../styles/page.css">
 </head>
 
 <body>
 
-<div id="panel">
-
-	<div id="header">
-		<h1><a href="https://threejs.org">three.js</a></h1>
-
-		<div id="sections">
-			<span class="selected">docs</span>
-			<a href="/examples/#webgl_animation_keyframes">examples</a>
-		</div>
-		<div id="expandButton"></div>
-	</div>
-
-	<div id="panelScrim"></div>
-
-	<div id="contentWrapper">
-		<div id="inputWrapper">
-			<input placeholder="" type="text" id="filterInput" autocorrect="off" autocapitalize="off" spellcheck="false" />
-			<div id="clearSearchButton"></div>
-		</div>
-		<div id="content">
-			<nav>
-				<?js= this.nav ?>
-			</nav>
-		</div>
-	</div>
-</div>
-
-<div id="viewer">
-	<div id="page">
-		<div id="page-content">
-			<?js if (augments && augments.length) {
-				var self = this;
-			?>
-				<p class="inheritance"><?js augments.forEach(function(a, i) { ?><?js= self.linkto(a, a) ?><?js if (i < augments.length - 1) { ?> → <?js } ?><?js }); ?> → </p>
-			<?js } ?>
-			<h1><?js= title ?></h1>
-			<?js= content ?>
-		</div>
-		<?js= this.partial('search.tmpl')?>
-	</div>
+<div id="page">
+	<?js if (augments && augments.length) {
+		var self = this;
+	?>
+		<p class="inheritance"><?js augments.forEach(function(a, i) { ?><?js= self.linkto(a, a) ?><?js if (i < augments.length - 1) { ?> → <?js } ?><?js }); ?> → </p>
+	<?js } ?>
+	<h1><?js= title ?></h1>
+	<?js= content ?>
 </div>
 
-<script src="scripts/page.js"></script>
-<script src="scripts/linenumber.js"></script>
-<script src="scripts/search.js"></script>
+<script src="../scripts/linenumber.js"></script>
+<script src="../scripts/page.js"></script>
 </body>
 </html>

+ 37 - 40
utils/docs/template/tmpl/members.tmpl

@@ -1,42 +1,39 @@
 <?js
-var data = obj;
-var self = this;
+	var data = obj;
+	var self = this;
 ?>
-<div class="member">
-	<h3 class="name" id="<?js= id ?>">.<a href="#<?js= id ?>"><?js= name ?></a><?js= (data.signature ? data.signature : '') ?> <?js= data.attribs ?></h3>
-
-	<?js if (data.summary) { ?>
-	<p class="summary"><?js= summary ?></p>
-	<?js } ?>
-
-	<?js if (data.description) { ?>
-	<div class="description">
-		<?js
-		var desc = data.description;
-		if (typeof data.defaultvalue !== 'undefined') {
-			var defaultText = '<br/>Default is <code>' + self.htmlsafe(data.defaultvalue) + '</code>.';
-			// If description ends with </p>, insert default before closing tag
-			if (desc.trim().endsWith('</p>')) {
-				desc = desc.trim().slice(0, -4) + defaultText + '</p>';
-			} else {
-				desc = desc + defaultText;
-			}
-		}
-		?><?js= desc ?>
-	</div>
-	<?js } ?>
-
-	<?js= this.partial('details.tmpl', data) ?>
-
-	<?js if (data.fires && fires.length) { ?>
-		<h5>Fires:</h5>
-		<ul><?js fires.forEach(function(f) { ?>
-			<li><?js= self.linkto(f) ?></li>
-		<?js }); ?></ul>
-	<?js } ?>
-
-	<?js if (data.examples && examples.length) { ?>
-		<h5>Example<?js= examples.length > 1? 's':'' ?></h5>
-		<?js= this.partial('examples.tmpl', examples) ?>
-	<?js } ?>
-</div>
+				<div class="member">
+					<h3 class="name" id="<?js= id ?>">.<a href="#<?js= id ?>"><?js= name ?></a><?js= (data.signature ? data.signature : '') ?> <?js= data.attribs ?></h3>
+					<?js if (data.summary) { ?>
+					<p class="summary"><?js= summary ?></p>
+					<?js } ?>
+					<?js if (data.description) { ?>
+					<div class="description">
+						<?js
+							var desc = data.description;
+							if (typeof data.defaultvalue !== 'undefined') {
+								var defaultText = '<br/>Default is <code>' + self.htmlsafe(data.defaultvalue) + '</code>.';
+								// If description ends with </p>, insert default before closing tag
+								if (desc.trim().endsWith('</p>')) {
+									desc = desc.trim().slice(0, -4) + defaultText + '</p>';
+								} else {
+									desc = desc + defaultText;
+								}
+							}
+						?><?js= desc ?>
+					</div>
+					<?js } ?>
+					<?js= this.partial('details.tmpl', data) ?>
+					<?js if (data.fires && fires.length) { ?>
+					<h5>Fires:</h5>
+					<ul>
+						<?js fires.forEach(function(f) { ?>
+						<li><?js= self.linkto(f) ?></li>
+						<?js }); ?>
+					</ul>
+					<?js } ?>
+					<?js if (data.examples && examples.length) { ?>
+					<h5>Example<?js= examples.length > 1? 's':'' ?></h5>
+					<?js= this.partial('examples.tmpl', examples) ?>
+					<?js } ?>
+				</div>

+ 71 - 76
utils/docs/template/tmpl/method.tmpl

@@ -1,79 +1,74 @@
 <?js
-var data = obj;
-var self = this;
+	var data = obj;
+	var self = this;
 ?>
-<?js if (data.kind !== 'module' && !data.hideconstructor) { ?>
-	<?js if (data.kind === 'class' && data.classdesc) { ?>
-	<h2>Constructor</h2>
+	<?js if (data.kind !== 'module' && !data.hideconstructor) { ?>
+		<?js if (data.kind === 'class' && data.classdesc) { ?>
+					<h2>Constructor</h2>
+		<?js } ?>
+					<h3 class="name name-method" id="<?js= id ?>"><?js= (kind === 'class' ? 'new ' : '.') ?><a href="#<?js= id ?>"><?js= name ?></a><?js= (data.signature || '') ?> <?js= data.attribs ?></h3>
+		<?js if (data.summary) { ?>
+					<p class="summary"><?js= summary ?></p>
+		<?js } ?>
 	<?js } ?>
-
-	<?js if (data.kind !== 'namespace') { ?>
-	<h3 class="name name-method" id="<?js= id ?>"><?js= (kind === 'class' ? 'new ' : '.') ?><a href="#<?js= id ?>"><?js= name ?></a><?js= (data.signature || '') ?> <?js= data.attribs ?></h3>
-	<?js } ?>
-
-	<?js if (data.summary) { ?>
-	<p class="summary"><?js= summary ?></p>
-	<?js } ?>
-<?js } ?>
-
-<div class="method">
-	<?js if (data.kind !== 'module' && data.description && !data.hideconstructor) { ?>
-	<div class="description">
-		<?js= data.description ?>
-	</div>
-	<?js } ?>
-
-	<?js if (data.augments && data.alias && data.alias.indexOf('module:') === 0) { ?>
-		<h5>Extends:</h5>
-		<?js= self.partial('augments.tmpl', data) ?>
-	<?js } ?>
-
-	<?js if (kind === 'event' && data.type && data.type.names) {?>
-		<h5>Type:</h5>
-		<ul>
-			<li>
-				<?js= self.partial('type.tmpl', data.type.names) ?>
-			</li>
-		</ul>
-	<?js } ?>
-
-	<?js if (data.params && params.length && !data.hideconstructor) { ?>
-		<?js= this.partial('params.tmpl', params) ?>
-	<?js } ?>
-
-	<?js= this.partial('details.tmpl', data) ?>
-
-	<?js if (data.fires && fires.length) { ?>
-	<h5>Fires:</h5>
-	<ul><?js fires.forEach(function(f) { ?>
-		<li><?js= self.linkto(f) ?></li>
-	<?js }); ?></ul>
-	<?js } ?>
-
-	<?js if (data.returns && returns.length) { ?>
-	<?js if (returns.length > 1) { ?>
-		<h5>Returns:</h5>
-		<ul><?js returns.forEach(function(r) { ?>
-			<li><?js= self.partial('returns.tmpl', r) ?></li>
-		<?js }); ?></ul>
-	<?js } else { ?>
-		<dl class="details">
-		<?js returns.forEach(function(r) { ?>
-			<dt class="tag-returns"><strong>Returns:</strong> <?js= self.partial('returns.tmpl', r) ?></dt>
-		<?js }); ?>
-		</dl>
-	<?js } } ?>
-
-	<?js if (data.yields && yields.length) { ?>
-	<h5>Yields:</h5>
-	<?js if (yields.length > 1) { ?><ul><?js
-		yields.forEach(function(r) { ?>
-			<li><?js= self.partial('returns.tmpl', r) ?></li>
-		<?js });
-	?></ul><?js } else {
-		yields.forEach(function(r) { ?>
-			<?js= self.partial('returns.tmpl', r) ?>
-		<?js });
-	} } ?>
-
-</div>
+					<div class="method">
+						<?js if (data.kind !== 'module' && data.description && !data.hideconstructor) { ?>
+						<div class="description">
+							<?js= data.description ?>
+						</div>
+						<?js } ?>
+						<?js if (data.augments && data.alias && data.alias.indexOf('module:') === 0) { ?>
+						<h5>Extends:</h5>
+						<?js= self.partial('augments.tmpl', data) ?>
+						<?js } ?>
+						<?js if (kind === 'event' && data.type && data.type.names) {?>
+						<h5>Type:</h5>
+						<ul>
+							<li>
+								<?js= self.partial('type.tmpl', data.type.names) ?>
+							</li>
+						</ul>
+						<?js } ?>
+						<?js if (data.params && params.length && !data.hideconstructor) { ?>
+							<?js= this.partial('params.tmpl', params) ?>
+						<?js } ?>
+						<?js= this.partial('details.tmpl', data) ?>
+						<?js if (data.fires && fires.length) { ?>
+						<h5>Fires:</h5>
+						<ul>
+							<?js fires.forEach(function(f) { ?>
+							<li><?js= self.linkto(f) ?></li>
+							<?js }); ?>
+						</ul>
+						<?js } ?>
+						<?js if (data.returns && returns.length) { ?>
+							<?js if (returns.length > 1) { ?>
+						<h5>Returns:</h5>
+						<ul>
+								<?js returns.forEach(function(r) { ?>
+							<li><?js= self.partial('returns.tmpl', r) ?></li>
+								<?js }); ?>
+						</ul>
+							<?js } else { ?>
+						<dl class="details">
+								<?js returns.forEach(function(r) { ?>
+							<dt class="tag-returns"><strong>Returns:</strong> <?js= self.partial('returns.tmpl', r) ?></dt>
+								<?js }); ?>
+						</dl>
+							<?js } ?>
+						<?js } ?>
+						<?js if (data.yields && yields.length) { ?>
+						<h5>Yields:</h5>
+							<?js if (yields.length > 1) { ?>
+						<ul>
+								<?js yields.forEach(function(r) { ?>
+							<li><?js= self.partial('returns.tmpl', r) ?></li>
+								<?js }); ?>
+						</ul>
+							<?js } else { ?>
+								<?js yields.forEach(function(r) { ?>
+							<?js= self.partial('returns.tmpl', r) ?>
+								<?js }); ?>
+							<?js } ?>
+						<?js } ?>
+					</div>

+ 30 - 34
utils/docs/template/tmpl/params.tmpl

@@ -46,37 +46,33 @@
 		}
 	});
 ?>
-
-<table class="params">
-	<tbody>
-	<?js
-		var self = this;
-		params.forEach(function(param) {
-			if (!param) { return; }
-	?>
-
-		<tr>
-			<?js if (params.hasName) {?>
-				<td class="name"><code><?js= param.name ?></code></td>
-			<?js } ?>
-
-			<td class="description last"><?js
-				var desc = param.description;
-				if (typeof param.defaultvalue !== 'undefined') {
-					var defaultText = '<br/>Default is <code>' + self.htmlsafe(param.defaultvalue) + '</code>.';
-					// If description ends with </p>, insert default before closing tag
-					if (desc.trim().endsWith('</p>')) {
-						desc = desc.trim().slice(0, -4) + defaultText + '</p>';
-					} else {
-						desc = desc + defaultText;
-					}
-				}
-			?><?js= desc ?><?js if (param.subparams) { ?>
-				<h6>Properties</h6>
-				<?js= self.partial('params.tmpl', param.subparams) ?>
-			<?js } ?></td>
-		</tr>
-
-	<?js }); ?>
-	</tbody>
-</table>
+						<table class="params">
+							<tbody>
+								<?js
+									var self = this;
+									params.forEach(function(param) {
+										if (!param) { return; }
+								?>
+								<tr>
+									<?js if (params.hasName) {?>
+									<td class="name"><code><?js= param.name ?></code></td>
+									<?js } ?>
+									<td class="description last"><?js
+										var desc = param.description;
+										if (typeof param.defaultvalue !== 'undefined') {
+											var defaultText = '<br/>Default is <code>' + self.htmlsafe(param.defaultvalue) + '</code>.';
+											// If description ends with </p>, insert default before closing tag
+											if (desc.trim().endsWith('</p>')) {
+												desc = desc.trim().slice(0, -4) + defaultText + '</p>';
+											} else {
+												desc = desc + defaultText;
+											}
+										}
+									?><?js= desc ?><?js if (param.subparams) { ?>
+										<h6>Properties</h6>
+										<?js= self.partial('params.tmpl', param.subparams) ?>
+									<?js } ?></td>
+								</tr>
+								<?js }); ?>
+							</tbody>
+						</table>

Некоторые файлы не были показаны из-за большого количества измененных файлов

粤ICP备19079148号