Upgrade vendored Three.js and MapLibre to latest releases.
CI / test (push) Successful in 4s

This updates Three.js to 0.183.2 and MapLibre GL to 5.19.0, and rewires GLTFLoader utility imports to local vendored modules for browser module compatibility.

Made-with: Cursor
This commit is contained in:
2026-03-02 22:48:19 +00:00
parent 7a16083a2e
commit aec12ff6c5
6 changed files with 4289 additions and 38235 deletions
File diff suppressed because one or more lines are too long
+4 -4
View File
File diff suppressed because one or more lines are too long
+385 -272
View File
File diff suppressed because it is too large Load Diff
+3239 -37850
View File
File diff suppressed because one or more lines are too long
+91 -29
View File
@@ -11,6 +11,30 @@ import {
Vector3, Vector3,
} from '../three.module.js'; } from '../three.module.js';
/**
* @module BufferGeometryUtils
* @three_import import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
*/
/**
* Computes vertex tangents using the MikkTSpace algorithm. MikkTSpace generates the same tangents consistently,
* and is used in most modelling tools and normal map bakers. Use MikkTSpace for materials with normal maps,
* because inconsistent tangents may lead to subtle visual issues in the normal map, particularly around mirrored
* UV seams.
*
* In comparison to this method, {@link BufferGeometry#computeTangents} (a custom algorithm) generates tangents that
* probably will not match the tangents in other software. The custom algorithm is sufficient for general use with a
* custom material, and may be faster than MikkTSpace.
*
* Returns the original BufferGeometry. Indexed geometries will be de-indexed. Requires position, normal, and uv attributes.
*
* @param {BufferGeometry} geometry - The geometry to compute tangents for.
* @param {Object} MikkTSpace - Instance of `examples/jsm/libs/mikktspace.module.js`, or `mikktspace` npm package.
* Await `MikkTSpace.ready` before use.
* @param {boolean} [negateSign=true] - Whether to negate the sign component (.w) of each tangent.
* Required for normal map conventions in some formats, including glTF.
* @return {BufferGeometry} The updated geometry.
*/
function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) { function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
if ( ! MikkTSpace || ! MikkTSpace.isReady ) { if ( ! MikkTSpace || ! MikkTSpace.isReady ) {
@@ -100,9 +124,11 @@ function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {
} }
/** /**
* @param {Array<BufferGeometry>} geometries * Merges a set of geometries into a single instance. All geometries must have compatible attributes.
* @param {Boolean} useGroups *
* @return {BufferGeometry} * @param {Array<BufferGeometry>} geometries - The geometries to merge.
* @param {boolean} [useGroups=false] - Whether to use groups or not.
* @return {?BufferGeometry} The merged geometry. Returns `null` if the merge does not succeed.
*/ */
function mergeGeometries( geometries, useGroups = false ) { function mergeGeometries( geometries, useGroups = false ) {
@@ -296,8 +322,11 @@ function mergeGeometries( geometries, useGroups = false ) {
} }
/** /**
* @param {Array<BufferAttribute>} attributes * Merges a set of attributes into a single instance. All attributes must have compatible properties and types.
* @return {BufferAttribute} * Instances of {@link InterleavedBufferAttribute} are not supported.
*
* @param {Array<BufferAttribute>} attributes - The attributes to merge.
* @return {?BufferAttribute} The merged attribute. Returns `null` if the merge does not succeed.
*/ */
function mergeAttributes( attributes ) { function mergeAttributes( attributes ) {
@@ -389,10 +418,12 @@ function mergeAttributes( attributes ) {
} }
/** /**
* @param {BufferAttribute} * Performs a deep clone of the given buffer attribute.
* @return {BufferAttribute} *
* @param {BufferAttribute} attribute - The attribute to clone.
* @return {BufferAttribute} The cloned attribute.
*/ */
export function deepCloneAttribute( attribute ) { function deepCloneAttribute( attribute ) {
if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) { if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {
@@ -411,8 +442,11 @@ export function deepCloneAttribute( attribute ) {
} }
/** /**
* @param {Array<BufferAttribute>} attributes * Interleaves a set of attributes and returns a new array of corresponding attributes that share a
* @return {Array<InterleavedBufferAttribute>} * single {@link InterleavedBuffer} instance. All attributes must have compatible types.
*
* @param {Array<BufferAttribute>} attributes - The attributes to interleave.
* @return {?Array<InterleavedBufferAttribute>} An array of interleaved attributes. If interleave does not succeed, the method returns `null`.
*/ */
function interleaveAttributes( attributes ) { function interleaveAttributes( attributes ) {
@@ -475,8 +509,13 @@ function interleaveAttributes( attributes ) {
} }
// returns a new, non-interleaved version of the provided attribute /**
export function deinterleaveAttribute( attribute ) { * Returns a new, non-interleaved version of the given attribute.
*
* @param {InterleavedBufferAttribute} attribute - The interleaved attribute.
* @return {BufferAttribute} The non-interleaved attribute.
*/
function deinterleaveAttribute( attribute ) {
const cons = attribute.data.array.constructor; const cons = attribute.data.array.constructor;
const count = attribute.count; const count = attribute.count;
@@ -523,8 +562,12 @@ export function deinterleaveAttribute( attribute ) {
} }
// deinterleaves all attributes on the geometry /**
export function deinterleaveGeometry( geometry ) { * Deinterleaves all attributes on the given geometry.
*
* @param {BufferGeometry} geometry - The geometry to deinterleave.
*/
function deinterleaveGeometry( geometry ) {
const attributes = geometry.attributes; const attributes = geometry.attributes;
const morphTargets = geometry.morphTargets; const morphTargets = geometry.morphTargets;
@@ -567,8 +610,10 @@ export function deinterleaveGeometry( geometry ) {
} }
/** /**
* @param {BufferGeometry} geometry * Returns the amount of bytes used by all attributes to represent the geometry.
* @return {number} *
* @param {BufferGeometry} geometry - The geometry.
* @return {number} The estimate bytes used.
*/ */
function estimateBytesUsed( geometry ) { function estimateBytesUsed( geometry ) {
@@ -590,9 +635,11 @@ function estimateBytesUsed( geometry ) {
} }
/** /**
* @param {BufferGeometry} geometry * Returns a new geometry with vertices for which all similar vertex attributes (within tolerance) are merged.
* @param {number} tolerance *
* @return {BufferGeometry} * @param {BufferGeometry} geometry - The geometry to merge vertices for.
* @param {number} [tolerance=1e-4] - The tolerance value.
* @return {BufferGeometry} - The new geometry with merged vertices.
*/ */
function mergeVertices( geometry, tolerance = 1e-4 ) { function mergeVertices( geometry, tolerance = 1e-4 ) {
@@ -753,9 +800,12 @@ function mergeVertices( geometry, tolerance = 1e-4 ) {
} }
/** /**
* @param {BufferGeometry} geometry * Returns a new indexed geometry based on `TrianglesDrawMode` draw mode.
* @param {number} drawMode * This mode corresponds to the `gl.TRIANGLES` primitive in WebGL.
* @return {BufferGeometry} *
* @param {BufferGeometry} geometry - The geometry to convert.
* @param {number} drawMode - The current draw mode.
* @return {BufferGeometry} The new geometry using `TrianglesDrawMode`.
*/ */
function toTrianglesDrawMode( geometry, drawMode ) { function toTrianglesDrawMode( geometry, drawMode ) {
@@ -864,9 +914,13 @@ function toTrianglesDrawMode( geometry, drawMode ) {
/** /**
* Calculates the morphed attributes of a morphed/skinned BufferGeometry. * Calculates the morphed attributes of a morphed/skinned BufferGeometry.
* Helpful for Raytracing or Decals. *
* @param {Mesh | Line | Points} object An instance of Mesh, Line or Points. * Helpful for Raytracing or Decals (i.e. a `DecalGeometry` applied to a morphed Object with a `BufferGeometry`
* @return {Object} An Object with original position/normal attributes and morphed ones. * will use the original `BufferGeometry`, not the morphed/skinned one, generating an incorrect result.
* Using this function to create a shadow `Object3`D the `DecalGeometry` can be correctly generated).
*
* @param {Mesh|Line|Points} object - The 3D object to compute morph attributes for.
* @return {Object} An object with original position/normal attributes and morphed ones.
*/ */
function computeMorphedAttributes( object ) { function computeMorphedAttributes( object ) {
@@ -1142,6 +1196,12 @@ function computeMorphedAttributes( object ) {
} }
/**
* Merges the {@link BufferGeometry#groups} for the given geometry.
*
* @param {BufferGeometry} geometry - The geometry to modify.
* @return {BufferGeometry} - The updated geometry
*/
function mergeGroups( geometry ) { function mergeGroups( geometry ) {
if ( geometry.groups.length === 0 ) { if ( geometry.groups.length === 0 ) {
@@ -1244,15 +1304,14 @@ function mergeGroups( geometry ) {
} }
/** /**
* Modifies the supplied geometry if it is non-indexed, otherwise creates a new, * Modifies the supplied geometry if it is non-indexed, otherwise creates a new,
* non-indexed geometry. Returns the geometry with smooth normals everywhere except * non-indexed geometry. Returns the geometry with smooth normals everywhere except
* faces that meet at an angle greater than the crease angle. * faces that meet at an angle greater than the crease angle.
* *
* @param {BufferGeometry} geometry * @param {BufferGeometry} geometry - The geometry to modify.
* @param {number} [creaseAngle] * @param {number} [creaseAngle=Math.PI/3] - The crease angle in radians.
* @return {BufferGeometry} * @return {BufferGeometry} - The updated geometry
*/ */
function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) { function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) {
@@ -1363,6 +1422,9 @@ export {
computeMikkTSpaceTangents, computeMikkTSpaceTangents,
mergeGeometries, mergeGeometries,
mergeAttributes, mergeAttributes,
deepCloneAttribute,
deinterleaveAttribute,
deinterleaveGeometry,
interleaveAttributes, interleaveAttributes,
estimateBytesUsed, estimateBytesUsed,
mergeVertices, mergeVertices,
+490
View File
@@ -0,0 +1,490 @@
import {
AnimationClip,
AnimationMixer,
Matrix4,
Quaternion,
QuaternionKeyframeTrack,
SkeletonHelper,
Vector3,
VectorKeyframeTrack
} from '../three.module.js';
/**
* @module SkeletonUtils
* @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
*/
function getBoneName( bone, options ) {
if ( options.getBoneName !== undefined ) {
return options.getBoneName( bone );
}
return options.names[ bone.name ];
}
/**
* Retargets the skeleton from the given source 3D object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
*/
function retarget( target, source, options = {} ) {
const quat = new Quaternion(),
scale = new Vector3(),
relativeMatrix = new Matrix4(),
globalMatrix = new Matrix4();
options.preserveBoneMatrix = options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true;
options.preserveBonePositions = options.preserveBonePositions !== undefined ? options.preserveBonePositions : true;
options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
options.hip = options.hip !== undefined ? options.hip : 'hip';
options.hipInfluence = options.hipInfluence !== undefined ? options.hipInfluence : new Vector3( 1, 1, 1 );
options.scale = options.scale !== undefined ? options.scale : 1;
options.names = options.names || {};
const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
bones = target.isObject3D ? target.skeleton.bones : getBones( target );
let bone, name, boneTo,
bonesPosition;
// reset bones
if ( target.isObject3D ) {
target.skeleton.pose();
} else {
options.useTargetMatrix = true;
options.preserveBoneMatrix = false;
}
if ( options.preserveBonePositions ) {
bonesPosition = [];
for ( let i = 0; i < bones.length; i ++ ) {
bonesPosition.push( bones[ i ].position.clone() );
}
}
if ( options.preserveBoneMatrix ) {
// reset matrix
target.updateMatrixWorld();
target.matrixWorld.identity();
// reset children matrix
for ( let i = 0; i < target.children.length; ++ i ) {
target.children[ i ].updateMatrixWorld( true );
}
}
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options );
boneTo = getBoneByName( name, sourceBones );
globalMatrix.copy( bone.matrixWorld );
if ( boneTo ) {
boneTo.updateMatrixWorld();
if ( options.useTargetMatrix ) {
relativeMatrix.copy( boneTo.matrixWorld );
} else {
relativeMatrix.copy( target.matrixWorld ).invert();
relativeMatrix.multiply( boneTo.matrixWorld );
}
// ignore scale to extract rotation
scale.setFromMatrixScale( relativeMatrix );
relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
// apply to global matrix
globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
if ( target.isObject3D ) {
if ( options.localOffsets ) {
if ( options.localOffsets[ bone.name ] ) {
globalMatrix.multiply( options.localOffsets[ bone.name ] );
}
}
}
globalMatrix.copyPosition( relativeMatrix );
}
if ( name === options.hip ) {
globalMatrix.elements[ 12 ] *= options.scale * options.hipInfluence.x;
globalMatrix.elements[ 13 ] *= options.scale * options.hipInfluence.y;
globalMatrix.elements[ 14 ] *= options.scale * options.hipInfluence.z;
if ( options.hipPosition !== undefined ) {
globalMatrix.elements[ 12 ] += options.hipPosition.x * options.scale;
globalMatrix.elements[ 13 ] += options.hipPosition.y * options.scale;
globalMatrix.elements[ 14 ] += options.hipPosition.z * options.scale;
}
}
if ( bone.parent ) {
bone.matrix.copy( bone.parent.matrixWorld ).invert();
bone.matrix.multiply( globalMatrix );
} else {
bone.matrix.copy( globalMatrix );
}
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
bone.updateMatrixWorld();
}
if ( options.preserveBonePositions ) {
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options ) || bone.name;
if ( name !== options.hip ) {
bone.position.copy( bonesPosition[ i ] );
}
}
}
if ( options.preserveBoneMatrix ) {
// restore matrix
target.updateMatrixWorld( true );
}
}
/**
* Retargets the animation clip of the source object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {AnimationClip} clip - The animation clip.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
* @return {AnimationClip} The retargeted animation clip.
*/
function retargetClip( target, source, clip, options = {} ) {
options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
options.names = options.names || [];
if ( ! source.isObject3D ) {
source = getHelperFromSkeleton( source );
}
const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
delta = clip.duration / ( numFrames - 1 ),
convertedTracks = [],
mixer = new AnimationMixer( source ),
bones = getBones( target.skeleton ),
boneDatas = [];
let positionOffset,
bone, boneTo, boneData,
name;
mixer.clipAction( clip ).play();
// trim
let start = 0, end = numFrames;
if ( options.trim !== undefined ) {
start = Math.round( options.trim[ 0 ] * options.fps );
end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;
mixer.update( options.trim[ 0 ] );
} else {
mixer.update( 0 );
}
source.updateMatrixWorld();
//
for ( let frame = 0; frame < end; ++ frame ) {
const time = frame * delta;
retarget( target, source, options );
for ( let j = 0; j < bones.length; ++ j ) {
bone = bones[ j ];
name = getBoneName( bone, options ) || bone.name;
boneTo = getBoneByName( name, source.skeleton );
if ( boneTo ) {
boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
if ( options.hip === name ) {
if ( ! boneData.pos ) {
boneData.pos = {
times: new Float32Array( end ),
values: new Float32Array( end * 3 )
};
}
if ( options.useFirstFramePosition ) {
if ( frame === 0 ) {
positionOffset = bone.position.clone();
}
bone.position.sub( positionOffset );
}
boneData.pos.times[ frame ] = time;
bone.position.toArray( boneData.pos.values, frame * 3 );
}
if ( ! boneData.quat ) {
boneData.quat = {
times: new Float32Array( end ),
values: new Float32Array( end * 4 )
};
}
boneData.quat.times[ frame ] = time;
bone.quaternion.toArray( boneData.quat.values, frame * 4 );
}
}
if ( frame === end - 2 ) {
// last mixer update before final loop iteration
// make sure we do not go over or equal to clip duration
mixer.update( delta - 0.0000001 );
} else {
mixer.update( delta );
}
source.updateMatrixWorld();
}
for ( let i = 0; i < boneDatas.length; ++ i ) {
boneData = boneDatas[ i ];
if ( boneData ) {
if ( boneData.pos ) {
convertedTracks.push( new VectorKeyframeTrack(
'.bones[' + boneData.bone.name + '].position',
boneData.pos.times,
boneData.pos.values
) );
}
convertedTracks.push( new QuaternionKeyframeTrack(
'.bones[' + boneData.bone.name + '].quaternion',
boneData.quat.times,
boneData.quat.values
) );
}
}
mixer.uncacheAction( clip );
return new AnimationClip( clip.name, - 1, convertedTracks );
}
/**
* Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are
* correctly associated with their bones. Bones are also cloned, and must be descendants of the
* object passed to this method. Other data, like geometries and materials, are reused by reference.
*
* @param {Object3D} source - The 3D object to clone.
* @return {Object3D} The cloned 3D object.
*/
function clone( source ) {
const sourceLookup = new Map();
const cloneLookup = new Map();
const clone = source.clone();
parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
sourceLookup.set( clonedNode, sourceNode );
cloneLookup.set( sourceNode, clonedNode );
} );
clone.traverse( function ( node ) {
if ( ! node.isSkinnedMesh ) return;
const clonedMesh = node;
const sourceMesh = sourceLookup.get( node );
const sourceBones = sourceMesh.skeleton.bones;
clonedMesh.skeleton = sourceMesh.skeleton.clone();
clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
return cloneLookup.get( bone );
} );
clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
} );
return clone;
}
// internal helper
function getBoneByName( name, skeleton ) {
for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) {
if ( name === bones[ i ].name )
return bones[ i ];
}
}
function getBones( skeleton ) {
return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
}
function getHelperFromSkeleton( skeleton ) {
const source = new SkeletonHelper( skeleton.bones[ 0 ] );
source.skeleton = skeleton;
return source;
}
function parallelTraverse( a, b, callback ) {
callback( a, b );
for ( let i = 0; i < a.children.length; i ++ ) {
parallelTraverse( a.children[ i ], b.children[ i ], callback );
}
}
/**
* Retarget options of `SkeletonUtils`.
*
* @typedef {Object} module:SkeletonUtils~RetargetOptions
* @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not.
* @property {number} [fps] - The FPS of the clip.
* @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names.
* @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`.
* @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end.
* @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not.
* @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not.
* @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not.
* @property {string} [hip='hip'] - The name of the source's hip bone.
* @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence.
* @property {number} [scale=1] - The scale.
**/
export {
retarget,
retargetClip,
clone,
};