three-viewport-gizmo
Version:
Three Viewport Gizmo is a highly customizable standalone interactive three.js view helper controls, allowing effortless integration with popular camera libraries.
1 lines • 173 kB
Source Map (JSON)
{"version":3,"file":"three-viewport-gizmo.umd.cjs","sources":["../lib/utils/gizmoDomElement.ts","../lib/utils/getDomElement.ts","../node_modules/three/src/math/MathUtils.js","../lib/utils/updateAxis.ts","../lib/utils/isClick.ts","../lib/utils/intersectedObjects.ts","../lib/utils/constants.ts","../lib/utils/updateBackground.ts","../lib/utils/deepClone.ts","../lib/utils/optionsFallback.ts","../lib/utils/axesMap.ts","../lib/utils/roundedRectangleGeometry.ts","../lib/utils/axesFaces.ts","../lib/utils/axesCorners.ts","../lib/utils/axesEdges.ts","../node_modules/three/examples/jsm/utils/BufferGeometryUtils.js","../lib/utils/gizmoBackground.ts","../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js","../node_modules/three/examples/jsm/lines/LineMaterial.js","../node_modules/three/examples/jsm/lines/LineSegments2.js","../node_modules/three/examples/jsm/lines/LineGeometry.js","../node_modules/three/examples/jsm/lines/Line2.js","../lib/utils/axesLines.ts","../lib/utils/axesObjects.ts","../lib/utils/axisHover.ts","../lib/ViewportGizmo.ts"],"sourcesContent":["import { GizmoOptionsFallback } from \"../types\";\n\nexport const setDomPlacement = (\n domElement: HTMLElement,\n placement: GizmoOptionsFallback[\"placement\"]\n) => {\n const [y, x] = placement.split(\"-\");\n Object.assign(domElement.style, {\n left: x === \"left\" ? \"0\" : x === \"center\" ? `50%` : \"\",\n right: x === \"right\" ? \"0\" : \"\",\n top: y === \"top\" ? \"0\" : y === \"bottom\" ? \"\" : \"50%\",\n bottom: y === \"bottom\" ? \"0\" : \"\",\n transform: `${x === \"center\" ? \"translateX(-50%)\" : \"\"} ${\n y === \"center\" ? \"translateY(-50%)\" : \"\"\n }`,\n });\n\n return placement;\n};\n\nexport const gizmoDomElement = ({\n placement,\n size,\n offset,\n id,\n className,\n}: GizmoOptionsFallback) => {\n const div = document.createElement(\"div\");\n\n const { top, left, right, bottom } = offset;\n Object.assign(div.style, {\n id,\n position: \"absolute\",\n zIndex: \"1000\",\n height: `${size}px`,\n width: `${size}px`,\n margin: `${top}px ${right}px ${bottom}px ${left}px`,\n borderRadius: \"100%\",\n });\n\n setDomPlacement(div, placement);\n\n if (id) div.id = id;\n if (className) div.className = className;\n\n return div;\n};\n","export const getDomElement = (\n domElement: string | HTMLElement\n): HTMLElement => {\n const element =\n typeof domElement === \"string\"\n ? document.querySelector<HTMLElement>(domElement)\n : domElement;\n\n if (!element) throw Error(`Invalid DOM element`);\n\n return element;\n};\n","const _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ];\n\nlet _seed = 1234567;\n\n\nconst DEG2RAD = Math.PI / 180;\nconst RAD2DEG = 180 / Math.PI;\n\n// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136\nfunction generateUUID() {\n\n\tconst d0 = Math.random() * 0xffffffff | 0;\n\tconst d1 = Math.random() * 0xffffffff | 0;\n\tconst d2 = Math.random() * 0xffffffff | 0;\n\tconst d3 = Math.random() * 0xffffffff | 0;\n\tconst uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +\n\t\t\t_lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +\n\t\t\t_lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +\n\t\t\t_lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];\n\n\t// .toLowerCase() here flattens concatenated strings to save heap memory space.\n\treturn uuid.toLowerCase();\n\n}\n\nfunction clamp( value, min, max ) {\n\n\treturn Math.max( min, Math.min( max, value ) );\n\n}\n\n// compute euclidean modulo of m % n\n// https://en.wikipedia.org/wiki/Modulo_operation\nfunction euclideanModulo( n, m ) {\n\n\treturn ( ( n % m ) + m ) % m;\n\n}\n\n// Linear mapping from range <a1, a2> to range <b1, b2>\nfunction mapLinear( x, a1, a2, b1, b2 ) {\n\n\treturn b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );\n\n}\n\n// https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/inverse-lerp-a-super-useful-yet-often-overlooked-function-r5230/\nfunction inverseLerp( x, y, value ) {\n\n\tif ( x !== y ) {\n\n\t\treturn ( value - x ) / ( y - x );\n\n\t} else {\n\n\t\treturn 0;\n\n\t}\n\n}\n\n// https://en.wikipedia.org/wiki/Linear_interpolation\nfunction lerp( x, y, t ) {\n\n\treturn ( 1 - t ) * x + t * y;\n\n}\n\n// http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/\nfunction damp( x, y, lambda, dt ) {\n\n\treturn lerp( x, y, 1 - Math.exp( - lambda * dt ) );\n\n}\n\n// https://www.desmos.com/calculator/vcsjnyz7x4\nfunction pingpong( x, length = 1 ) {\n\n\treturn length - Math.abs( euclideanModulo( x, length * 2 ) - length );\n\n}\n\n// http://en.wikipedia.org/wiki/Smoothstep\nfunction smoothstep( x, min, max ) {\n\n\tif ( x <= min ) return 0;\n\tif ( x >= max ) return 1;\n\n\tx = ( x - min ) / ( max - min );\n\n\treturn x * x * ( 3 - 2 * x );\n\n}\n\nfunction smootherstep( x, min, max ) {\n\n\tif ( x <= min ) return 0;\n\tif ( x >= max ) return 1;\n\n\tx = ( x - min ) / ( max - min );\n\n\treturn x * x * x * ( x * ( x * 6 - 15 ) + 10 );\n\n}\n\n// Random integer from <low, high> interval\nfunction randInt( low, high ) {\n\n\treturn low + Math.floor( Math.random() * ( high - low + 1 ) );\n\n}\n\n// Random float from <low, high> interval\nfunction randFloat( low, high ) {\n\n\treturn low + Math.random() * ( high - low );\n\n}\n\n// Random float from <-range/2, range/2> interval\nfunction randFloatSpread( range ) {\n\n\treturn range * ( 0.5 - Math.random() );\n\n}\n\n// Deterministic pseudo-random float in the interval [ 0, 1 ]\nfunction seededRandom( s ) {\n\n\tif ( s !== undefined ) _seed = s;\n\n\t// Mulberry32 generator\n\n\tlet t = _seed += 0x6D2B79F5;\n\n\tt = Math.imul( t ^ t >>> 15, t | 1 );\n\n\tt ^= t + Math.imul( t ^ t >>> 7, t | 61 );\n\n\treturn ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296;\n\n}\n\nfunction degToRad( degrees ) {\n\n\treturn degrees * DEG2RAD;\n\n}\n\nfunction radToDeg( radians ) {\n\n\treturn radians * RAD2DEG;\n\n}\n\nfunction isPowerOfTwo( value ) {\n\n\treturn ( value & ( value - 1 ) ) === 0 && value !== 0;\n\n}\n\nfunction ceilPowerOfTwo( value ) {\n\n\treturn Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );\n\n}\n\nfunction floorPowerOfTwo( value ) {\n\n\treturn Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );\n\n}\n\nfunction setQuaternionFromProperEuler( q, a, b, c, order ) {\n\n\t// Intrinsic Proper Euler Angles - see https://en.wikipedia.org/wiki/Euler_angles\n\n\t// rotations are applied to the axes in the order specified by 'order'\n\t// rotation by angle 'a' is applied first, then by angle 'b', then by angle 'c'\n\t// angles are in radians\n\n\tconst cos = Math.cos;\n\tconst sin = Math.sin;\n\n\tconst c2 = cos( b / 2 );\n\tconst s2 = sin( b / 2 );\n\n\tconst c13 = cos( ( a + c ) / 2 );\n\tconst s13 = sin( ( a + c ) / 2 );\n\n\tconst c1_3 = cos( ( a - c ) / 2 );\n\tconst s1_3 = sin( ( a - c ) / 2 );\n\n\tconst c3_1 = cos( ( c - a ) / 2 );\n\tconst s3_1 = sin( ( c - a ) / 2 );\n\n\tswitch ( order ) {\n\n\t\tcase 'XYX':\n\t\t\tq.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'YZY':\n\t\t\tq.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'ZXZ':\n\t\t\tq.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'XZX':\n\t\t\tq.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'YXY':\n\t\t\tq.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );\n\t\t\tbreak;\n\n\t\tcase 'ZYZ':\n\t\t\tq.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );\n\t\t\tbreak;\n\n\t\tdefault:\n\t\t\tconsole.warn( 'THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ' + order );\n\n\t}\n\n}\n\nfunction denormalize( value, array ) {\n\n\tswitch ( array.constructor ) {\n\n\t\tcase Float32Array:\n\n\t\t\treturn value;\n\n\t\tcase Uint32Array:\n\n\t\t\treturn value / 4294967295.0;\n\n\t\tcase Uint16Array:\n\n\t\t\treturn value / 65535.0;\n\n\t\tcase Uint8Array:\n\n\t\t\treturn value / 255.0;\n\n\t\tcase Int32Array:\n\n\t\t\treturn Math.max( value / 2147483647.0, - 1.0 );\n\n\t\tcase Int16Array:\n\n\t\t\treturn Math.max( value / 32767.0, - 1.0 );\n\n\t\tcase Int8Array:\n\n\t\t\treturn Math.max( value / 127.0, - 1.0 );\n\n\t\tdefault:\n\n\t\t\tthrow new Error( 'Invalid component type.' );\n\n\t}\n\n}\n\nfunction normalize( value, array ) {\n\n\tswitch ( array.constructor ) {\n\n\t\tcase Float32Array:\n\n\t\t\treturn value;\n\n\t\tcase Uint32Array:\n\n\t\t\treturn Math.round( value * 4294967295.0 );\n\n\t\tcase Uint16Array:\n\n\t\t\treturn Math.round( value * 65535.0 );\n\n\t\tcase Uint8Array:\n\n\t\t\treturn Math.round( value * 255.0 );\n\n\t\tcase Int32Array:\n\n\t\t\treturn Math.round( value * 2147483647.0 );\n\n\t\tcase Int16Array:\n\n\t\t\treturn Math.round( value * 32767.0 );\n\n\t\tcase Int8Array:\n\n\t\t\treturn Math.round( value * 127.0 );\n\n\t\tdefault:\n\n\t\t\tthrow new Error( 'Invalid component type.' );\n\n\t}\n\n}\n\nconst MathUtils = {\n\tDEG2RAD: DEG2RAD,\n\tRAD2DEG: RAD2DEG,\n\tgenerateUUID: generateUUID,\n\tclamp: clamp,\n\teuclideanModulo: euclideanModulo,\n\tmapLinear: mapLinear,\n\tinverseLerp: inverseLerp,\n\tlerp: lerp,\n\tdamp: damp,\n\tpingpong: pingpong,\n\tsmoothstep: smoothstep,\n\tsmootherstep: smootherstep,\n\trandInt: randInt,\n\trandFloat: randFloat,\n\trandFloatSpread: randFloatSpread,\n\tseededRandom: seededRandom,\n\tdegToRad: degToRad,\n\tradToDeg: radToDeg,\n\tisPowerOfTwo: isPowerOfTwo,\n\tceilPowerOfTwo: ceilPowerOfTwo,\n\tfloorPowerOfTwo: floorPowerOfTwo,\n\tsetQuaternionFromProperEuler: setQuaternionFromProperEuler,\n\tnormalize: normalize,\n\tdenormalize: denormalize\n};\n\nexport {\n\tDEG2RAD,\n\tRAD2DEG,\n\tgenerateUUID,\n\tclamp,\n\teuclideanModulo,\n\tmapLinear,\n\tinverseLerp,\n\tlerp,\n\tdamp,\n\tpingpong,\n\tsmoothstep,\n\tsmootherstep,\n\trandInt,\n\trandFloat,\n\trandFloatSpread,\n\tseededRandom,\n\tdegToRad,\n\tradToDeg,\n\tisPowerOfTwo,\n\tceilPowerOfTwo,\n\tfloorPowerOfTwo,\n\tsetQuaternionFromProperEuler,\n\tnormalize,\n\tdenormalize,\n\tMathUtils\n};\n","import { GizmoAxisObject, GizmoOptionsFallback } from \"@lib/types\";\nimport { Camera, Vector3 } from \"three\";\nimport { clamp } from \"three/src/math/MathUtils.js\";\n\nconst axisMap: [\n axis: \"x\" | \"y\" | \"z\",\n positiveIndex: number,\n negativeIndex: number\n][] = [\n [\"x\", 0, 3],\n [\"y\", 1, 4],\n [\"z\", 2, 5],\n];\n\nconst point = /*@__PURE__*/ new Vector3();\nexport function updateAxis(\n { isSphere }: GizmoOptionsFallback,\n axes: GizmoAxisObject[],\n camera: Camera\n): void {\n if (!isSphere) return;\n\n point.set(0, 0, 1).applyQuaternion(camera.quaternion);\n\n axisMap.forEach(([axis, positiveIndex, negativeIndex]) => {\n const value = point[axis];\n\n let object = axes[positiveIndex];\n let opacity = object.userData.opacity;\n\n object.material.opacity = clamp(value >= 0 ? opacity : opacity / 2, 0, 1);\n\n object = axes[negativeIndex];\n opacity = object.userData.opacity;\n\n object.material.opacity = clamp(value >= 0 ? opacity / 2 : opacity, 0, 1);\n });\n}\n","import { Vector2 } from \"three\";\n\nexport const isClick = (\n e: PointerEvent,\n startCoords: Vector2,\n threshold: number = 10\n) =>\n Math.abs(e.clientX - startCoords.x) < threshold &&\n Math.abs(e.clientY - startCoords.y) < threshold;\n","import { GizmoAxisObject } from \"@lib/types\";\nimport {\n Vector2,\n Raycaster,\n type Object3D,\n type Camera,\n Intersection,\n} from \"three\";\n\nconst _raycaster = /*@__PURE__*/ new Raycaster();\nconst _mouse = /*@__PURE__*/ new Vector2();\n\nexport const intersectedObjects = (\n event: PointerEvent,\n domRect: DOMRect,\n camera: Camera,\n intersections: Object3D[]\n): Intersection<GizmoAxisObject> | null => {\n _mouse.set(\n ((event.clientX - domRect.left) / domRect.width) * 2 - 1,\n -((event.clientY - domRect.top) / domRect.height) * 2 + 1\n );\n\n _raycaster.setFromCamera(_mouse, camera);\n\n const intersects = _raycaster.intersectObjects<GizmoAxisObject>(\n intersections,\n false\n );\n\n const intersection = intersects.length ? intersects[0] : null;\n\n return !intersection || !intersection.object.visible ? null : intersection;\n};\n","export const GIZMO_EPSILON = 1e-6;\nexport const GIZMO_TURN_RATE = 2 * Math.PI;\nexport const GIZMO_MAIN_AXES = [\"x\", \"y\", \"z\"] as const;\nexport const GIZMO_AXES = [...GIZMO_MAIN_AXES, \"nx\", \"ny\", \"nz\"] as const;\nexport const GIZMO_AXES_Z_UP = [\"x\", \"z\", \"y\", \"nx\", \"nz\", \"ny\"] as const;\nexport const GIZMO_AXES_X_UP = [\"z\", \"x\", \"y\", \"nz\", \"nx\", \"ny\"] as const;\nexport const GIZMO_FACE_RIGHT = \"Right\";\nexport const GIZMO_FACE_TOP = \"Top\";\nexport const GIZMO_FACE_FRONT = \"Front\";\nexport const GIZMO_FACE_LEFT = \"Left\";\nexport const GIZMO_FACE_BOTTOM = \"Bottom\";\nexport const GIZMO_FACE_BACK = \"Back\";\nexport const GIZMO_FACES = [\n GIZMO_FACE_RIGHT,\n GIZMO_FACE_TOP,\n GIZMO_FACE_FRONT,\n GIZMO_FACE_LEFT,\n GIZMO_FACE_BOTTOM,\n GIZMO_FACE_BACK,\n].map((face) => face.toLocaleLowerCase());\nexport const GIZMO_SPHERE_AXES_DISTANCE = 1.3;\n","import { BufferGeometry, Mesh, MeshBasicMaterial } from \"three\";\n\nexport const updateBackground = (\n background: Mesh<BufferGeometry, MeshBasicMaterial>,\n hovered: boolean = true\n) => {\n const { material, userData } = background;\n const { color, opacity } = hovered ? userData.hover : userData;\n\n material.color.set(color);\n material.opacity = opacity;\n};\n","export const deepClone = <T>(obj: T) => JSON.parse(JSON.stringify(obj));\n","import {\n GizmoAxisOptions,\n GizmoOptions,\n GizmoOptionsFallback,\n} from \"@lib/types\";\nimport {\n GIZMO_MAIN_AXES,\n GIZMO_AXES,\n GIZMO_AXES_X_UP,\n GIZMO_AXES_Z_UP,\n GIZMO_FACES,\n GIZMO_FACE_TOP,\n GIZMO_FACE_RIGHT,\n GIZMO_FACE_FRONT,\n GIZMO_FACE_BOTTOM,\n GIZMO_FACE_LEFT,\n GIZMO_FACE_BACK,\n} from \"./constants\";\n\nimport { deepClone } from \"./deepClone\";\nimport { Object3D } from \"three\";\n\nexport const optionsFallback = (\n options: GizmoOptions\n): GizmoOptionsFallback => {\n const type = options.type || \"sphere\";\n const isSphere = type === \"sphere\";\n const resolution = options.resolution || isSphere ? 64 : 128;\n\n const defaultUp = Object3D.DEFAULT_UP;\n const zUp = defaultUp.z === 1;\n const xUp = defaultUp.x === 1;\n\n const { container } = options;\n options.container = undefined;\n options = JSON.parse(JSON.stringify(options));\n options.container = container;\n\n // Convert face axis to regular axis\n const faceAxis = zUp ? GIZMO_AXES_Z_UP : xUp ? GIZMO_AXES_X_UP : GIZMO_AXES;\n GIZMO_FACES.forEach((face, index) => {\n if ((options as any)[face])\n options[faceAxis[index]] = (options as any)[face];\n });\n\n // Positive Axes fallback options\n const axesFallback: GizmoAxisOptions = {\n enabled: true,\n color: 0xffffff,\n opacity: 1,\n scale: isSphere ? 0.7 : 0.7,\n labelColor: 0x222222,\n line: false,\n border: {\n size: 0,\n color: 0xdddddd,\n },\n hover: {\n color: isSphere ? 0xffffff : 0x93d3eb,\n labelColor: 0x222222,\n opacity: 1,\n scale: isSphere ? 0.7 : 0.7,\n border: {\n size: 0,\n color: 0xdddddd,\n },\n },\n };\n\n // Negative Axes fallback options\n const negativeAxesFallback = {\n line: false,\n scale: isSphere ? 0.45 : 0.7,\n hover: {\n scale: isSphere ? 0.5 : 0.7,\n },\n };\n\n const optionsFallback: GizmoOptions = {\n type,\n container: document.body,\n size: 128,\n placement: \"top-right\",\n resolution,\n lineWidth: 4,\n radius: isSphere ? 1 : 0.2,\n smoothness: 18,\n animated: true,\n speed: 1,\n background: {\n enabled: true,\n color: isSphere ? 0xffffff : 0xe0e6ec,\n opacity: isSphere ? 0 : 1,\n hover: {\n color: isSphere ? 0xffffff : 0xe0e6ec,\n opacity: isSphere ? 0.2 : 1,\n },\n },\n font: {\n family: \"sans-serif\",\n weight: 900,\n },\n offset: {\n top: 10,\n left: 10,\n bottom: 10,\n right: 10,\n },\n corners: {\n enabled: !isSphere,\n color: isSphere ? 0xf2d962 : 0xffffff,\n opacity: 1,\n scale: isSphere ? 0.15 : 0.2,\n radius: 1,\n smoothness: 18,\n hover: {\n color: isSphere ? 0xffffff : 0x93d3eb,\n opacity: 1,\n scale: isSphere ? 0.2 : 0.225,\n },\n },\n edges: {\n enabled: !isSphere,\n color: isSphere ? 0xf2d962 : 0xffffff,\n opacity: isSphere ? 1 : 0,\n radius: isSphere ? 1 : 0.125,\n smoothness: 18,\n scale: isSphere ? 0.15 : 1,\n hover: {\n color: isSphere ? 0xffffff : 0x93d3eb,\n opacity: 1,\n scale: isSphere ? 0.2 : 1,\n },\n },\n x: {\n ...deepClone(axesFallback),\n ...(isSphere\n ? { label: \"X\", color: 0xff3653, line: true }\n : { label: xUp ? GIZMO_FACE_TOP : GIZMO_FACE_RIGHT }),\n },\n y: {\n ...deepClone(axesFallback),\n ...(isSphere\n ? { label: \"Y\", color: 0x8adb00, line: true }\n : { label: zUp || xUp ? GIZMO_FACE_FRONT : GIZMO_FACE_TOP }),\n },\n z: {\n ...deepClone(axesFallback),\n ...(isSphere\n ? { label: \"Z\", color: 0x2c8fff, line: true }\n : {\n label: zUp\n ? GIZMO_FACE_TOP\n : xUp\n ? GIZMO_FACE_RIGHT\n : GIZMO_FACE_FRONT,\n }),\n },\n nx: {\n ...deepClone(negativeAxesFallback),\n label: isSphere ? \"\" : xUp ? GIZMO_FACE_BOTTOM : GIZMO_FACE_LEFT,\n },\n ny: {\n ...deepClone(negativeAxesFallback),\n label: isSphere ? \"\" : zUp || xUp ? GIZMO_FACE_BACK : GIZMO_FACE_BOTTOM,\n },\n nz: {\n ...deepClone(negativeAxesFallback),\n label: isSphere\n ? \"\"\n : zUp\n ? GIZMO_FACE_BOTTOM\n : xUp\n ? GIZMO_FACE_LEFT\n : GIZMO_FACE_BACK,\n },\n };\n\n assignNestedDefaults(options, optionsFallback);\n\n // Negative axis fallback to positive axis\n GIZMO_MAIN_AXES.forEach((axis) =>\n assignNestedDefaults(\n (options as any)[`n${axis}`],\n deepClone((options as any)[axis])\n )\n );\n\n return { ...options, isSphere } as GizmoOptionsFallback;\n};\n\nfunction assignNestedDefaults<T>(target: T, ...defaultObjects: T[]) {\n if (\n target instanceof HTMLElement ||\n typeof target !== \"object\" ||\n target === null\n )\n return target;\n\n for (const defaults of defaultObjects) {\n for (const key in defaults) {\n if (key === \"container\") continue;\n\n if (key in (defaults as any)) {\n if (target[key] === undefined) {\n (target as any)[key] = defaults[key];\n } else if (\n typeof defaults[key] === \"object\" &&\n !Array.isArray(defaults[key])\n ) {\n (target as any)[key] = assignNestedDefaults(\n (target as any)[key] || {},\n defaults[key]\n );\n }\n }\n }\n }\n\n return target as T;\n}\n","import {\n CanvasTexture,\n Color,\n ColorRepresentation,\n RepeatWrapping,\n SRGBColorSpace,\n Texture,\n} from \"three\";\nimport { GizmoAxisOptions, GizmoOptionsFallback } from \"@lib/types\";\nimport { GIZMO_AXES } from \"./constants\";\n\nexport const axesMap = (options: GizmoOptionsFallback, offset: number = 2) => {\n const colorManager = new Color();\n const doubleOffset = offset * 2;\n const { isSphere, resolution, radius, font, corners, edges } = options;\n\n const axes: (Required<GizmoAxisOptions> & { radius: number })[] =\n GIZMO_AXES.map((axis) => ({ ...options[axis], radius }));\n\n if (isSphere && corners.enabled) axes.push(corners as any);\n if (isSphere && edges.enabled) axes.push(edges as any);\n\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\") as CanvasRenderingContext2D;\n\n canvas.width = resolution * 2 + doubleOffset * 2;\n canvas.height = resolution * axes.length + doubleOffset * axes.length;\n\n const [fontStyle, fontYFix] = getFontStyle(axes, resolution, font);\n\n axes.forEach(\n (\n {\n radius,\n label,\n color,\n labelColor,\n border,\n hover: {\n color: hoverColor,\n labelColor: hoverLabel,\n border: hoverBorder,\n },\n },\n index\n ) => {\n const y = resolution * index + index * doubleOffset + offset;\n drawAxis(\n offset,\n y,\n offset,\n resolution,\n radius,\n label,\n border,\n color,\n labelColor\n );\n drawAxis(\n resolution + offset * 3,\n y,\n offset,\n resolution,\n radius,\n label,\n hoverBorder ?? border,\n hoverColor ?? color,\n hoverLabel ?? labelColor\n );\n }\n );\n\n /* // Debug\n document.body.appendChild(canvas);\n Object.assign(canvas.style, {\n position: \"fixed\",\n top: 0,\n left: 0,\n zIndex: 100000,\n }); */\n\n const colsCount = axes.length;\n const offsetX = offset / (resolution * 2);\n const offsetY = offset / (resolution * 6);\n const cellHeight = 1 / colsCount;\n\n const map = new CanvasTexture(canvas);\n map.repeat.set(0.5 - 2 * offsetX, cellHeight - 2 * offsetY);\n map.offset.set(offsetX, 1 - offsetY);\n\n Object.assign(map, {\n colorSpace: SRGBColorSpace,\n wrapS: RepeatWrapping,\n wrapT: RepeatWrapping,\n userData: {\n offsetX,\n offsetY,\n cellHeight,\n },\n });\n\n return map;\n\n function drawAxis(\n x: number,\n y: number,\n offset: number,\n size: number,\n radius: number,\n label: string,\n border: GizmoAxisOptions[\"border\"],\n color: ColorRepresentation,\n labelColor: ColorRepresentation\n ) {\n radius = radius * (size / 2);\n\n if (color != null && color !== \"\") {\n drawRoundRectPath();\n ctx.fillStyle = colorManager.set(color!).getStyle();\n ctx.fill();\n }\n\n if (border && border.size) {\n const halfBorderWidth = (border.size * size) / 2;\n x += halfBorderWidth;\n y += halfBorderWidth;\n size -= border.size * size;\n radius = Math.max(0, radius - halfBorderWidth);\n\n drawRoundRectPath();\n ctx.strokeStyle = colorManager.set(border.color).getStyle();\n ctx.lineWidth = border.size * size;\n ctx.stroke();\n }\n\n if (label)\n drawText(\n ctx,\n x + size / 2,\n y + (size + offset) / 2,\n label,\n colorManager.set(labelColor!).getStyle()\n );\n\n function drawRoundRectPath() {\n ctx.beginPath();\n ctx.moveTo(x + radius, y);\n ctx.lineTo(x + size - radius, y);\n ctx.arcTo(x + size, y, x + size, y + radius, radius);\n ctx.lineTo(x + size, y + size - radius);\n ctx.arcTo(x + size, y + size, x + size - radius, y + size, radius);\n ctx.lineTo(x + radius, y + size);\n ctx.arcTo(x, y + size, x, y + size - radius, radius);\n ctx.lineTo(x, y + radius);\n ctx.arcTo(x, y, x + radius, y, radius);\n ctx.closePath();\n }\n }\n\n function getFontStyle(\n axis: GizmoAxisOptions[],\n resolution: number,\n font: GizmoOptionsFallback[\"font\"]\n ) {\n const longestLabel = [...axis]\n .sort((a, b) => (a.label?.length || 0) - (b.label?.length || 0))\n .pop()!;\n const text = longestLabel.label!;\n\n const { family, weight } = font;\n\n const square = isSphere\n ? Math.sqrt(Math.pow(resolution * 0.7, 2) / 2)\n : resolution;\n let fontSize = square;\n let textWidth = 0;\n let textHeight = 0;\n\n do {\n ctx.font = `${weight} ${fontSize}px ${family}`;\n const measure = ctx.measureText(text);\n textWidth = measure.width;\n textHeight = measure.fontBoundingBoxDescent;\n fontSize--;\n } while (textWidth > square && fontSize > 0);\n\n const yFix = square / textHeight;\n const scaleFactor = Math.min(square / textWidth, yFix);\n const finalFontSize = Math.floor(fontSize * scaleFactor);\n\n return [`${weight} ${finalFontSize}px ${family}`, yFix] as const;\n }\n\n function drawText(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n text: string,\n color: string\n ) {\n ctx.font = fontStyle;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillStyle = color;\n ctx.fillText(text, x, y + (isSphere ? fontYFix : 0));\n }\n};\n\nexport const setMapHoverOffset = (map: Texture, hover: boolean) =>\n (map.offset.x = (hover ? 0.5 : 0) + map.userData.offsetX);\n\nexport const setMapColumnOffset = (map: Texture, col: number) => {\n const {\n offset,\n userData: { offsetY, cellHeight },\n } = map;\n offset.y = 1 - (col + 1) * cellHeight + offsetY;\n};\n","import { BufferGeometry, BufferAttribute } from \"three\";\n\n/**\n * Generate a Rounded Rectangle geometry\n *\n * @param radius - The edges radius\n * @param smoothness - The edges smoothness\n *\n * @ThanksTo `@hofk` for the {@link https://discourse.threejs.org/t/roundedrectangle-squircle/28645 RoundedRectangle + Squircle } Geometry\n *\n * @returns A round rectangle geometry\n */\nexport function roundedRectangleGeometry(\n radius: number,\n smoothness: number,\n width: number = 2,\n height: number = 2\n) {\n // helper const's\n const wi = width / 2 - radius; // inner width\n const hi = height / 2 - radius; // inner height\n const ul = radius / width; // u left\n const ur = (width - radius) / width; // u right\n const vl = radius / height; // v low\n const vh = (height - radius) / height; // v high\n\n const positions = [wi, hi, 0, -wi, hi, 0, -wi, -hi, 0, wi, -hi, 0];\n const uvs = [ur, vh, ul, vh, ul, vl, ur, vl];\n const n = [\n 3 * (smoothness + 1) + 3,\n 3 * (smoothness + 1) + 4,\n smoothness + 4,\n smoothness + 5,\n 2 * (smoothness + 1) + 4,\n 2,\n 1,\n 2 * (smoothness + 1) + 3,\n 3,\n 4 * (smoothness + 1) + 3,\n 4,\n 0,\n ];\n const indices = [0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11].map(\n (i) => n[i]\n );\n\n let phi, cos, sin, xc, yc, uc, vc, idx;\n\n for (let i = 0; i < 4; i++) {\n xc = i < 1 || i > 2 ? wi : -wi;\n yc = i < 2 ? hi : -hi;\n\n uc = i < 1 || i > 2 ? ur : ul;\n vc = i < 2 ? vh : vl;\n\n for (let j = 0; j <= smoothness; j++) {\n phi = (Math.PI / 2) * (i + j / smoothness);\n cos = Math.cos(phi);\n sin = Math.sin(phi);\n\n positions.push(xc + radius * cos, yc + radius * sin, 0);\n\n uvs.push(uc + ul * cos, vc + vl * sin);\n\n if (j < smoothness) {\n idx = (smoothness + 1) * i + j + 4;\n indices.push(i, idx, idx + 1);\n }\n }\n }\n\n return new BufferGeometry()\n .setIndex(new BufferAttribute(new Uint32Array(indices), 1))\n .setAttribute(\n \"position\",\n new BufferAttribute(new Float32Array(positions), 3)\n )\n .setAttribute(\"uv\", new BufferAttribute(new Float32Array(uvs), 2));\n}\n","import { GizmoAxisObject, GizmoOptionsFallback } from \"@lib/types\";\nimport { roundedRectangleGeometry } from \"./roundedRectangleGeometry\";\nimport {\n CanvasTexture,\n Mesh,\n MeshBasicMaterial,\n MeshBasicMaterialParameters,\n Sprite,\n SpriteMaterial,\n Vector3,\n} from \"three\";\nimport { GIZMO_AXES, GIZMO_SPHERE_AXES_DISTANCE } from \"./constants\";\n\nimport { setMapColumnOffset } from \"./axesMap\";\n\nexport const axesFaces = (\n options: GizmoOptionsFallback,\n texture: CanvasTexture\n): GizmoAxisObject[] => {\n const target = new Vector3();\n const { isSphere, radius, smoothness } = options;\n const geometry = roundedRectangleGeometry(radius, smoothness);\n\n return GIZMO_AXES.map((_, i) => {\n const isPositive = i < 3;\n\n const axis = GIZMO_AXES[i];\n const map = i ? texture.clone() : texture;\n\n setMapColumnOffset(map, i);\n\n const { enabled, scale, opacity, hover } = options[axis];\n\n const materialConfig: MeshBasicMaterialParameters = {\n map,\n opacity,\n transparent: true,\n };\n\n const face = isSphere\n ? new Sprite(new SpriteMaterial(materialConfig))\n : new Mesh(geometry, new MeshBasicMaterial(materialConfig));\n\n const direction = (isPositive ? axis : axis[1]) as \"x\" | \"y\" | \"z\";\n face.position[direction] =\n (isPositive ? 1 : -1) * (isSphere ? GIZMO_SPHERE_AXES_DISTANCE : 1);\n\n if (!isSphere) face.lookAt(target.copy(face.position).multiplyScalar(1.7));\n\n face.scale.setScalar(scale);\n face.renderOrder = 1;\n face.visible = enabled;\n face.userData = {\n scale,\n opacity,\n hover,\n };\n\n return face;\n });\n};\n","import { GizmoAxisObject, GizmoOptionsFallback } from \"@lib/types\";\nimport {\n CanvasTexture,\n Mesh,\n MeshBasicMaterial,\n MeshBasicMaterialParameters,\n Sprite,\n SpriteMaterial,\n Vector3,\n} from \"three\";\nimport { roundedRectangleGeometry } from \"./roundedRectangleGeometry\";\nimport { setMapColumnOffset } from \"./axesMap\";\n\nexport const axesCorners = (\n options: GizmoOptionsFallback,\n texture: CanvasTexture\n) => {\n const { isSphere, corners } = options;\n\n if (!corners.enabled) return [];\n\n const { color, opacity, scale, radius, smoothness, hover } = corners;\n\n const geometry = isSphere\n ? null\n : roundedRectangleGeometry(radius, smoothness);\n\n const materialConfig: MeshBasicMaterialParameters = {\n transparent: true,\n opacity,\n };\n\n const positions = [\n 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1,\n -1, -1,\n ].map((val) => val * 0.85);\n\n const target = new Vector3();\n return Array(positions.length / 3)\n .fill(0)\n .map<GizmoAxisObject>((_, i) => {\n if (isSphere) {\n const map = texture.clone();\n setMapColumnOffset(map, 6);\n materialConfig.map = map;\n } else {\n materialConfig.color = color;\n }\n\n const corner = isSphere\n ? new Sprite(new SpriteMaterial(materialConfig))\n : new Mesh(geometry!, new MeshBasicMaterial(materialConfig));\n\n const i3 = i * 3;\n corner.position.set(positions[i3], positions[i3 + 1], positions[i3 + 2]);\n\n if (isSphere) corner.position.normalize().multiplyScalar(1.7);\n\n corner.scale.setScalar(scale);\n corner.lookAt(target.copy(corner.position).multiplyScalar(2));\n corner.renderOrder = 1;\n\n corner.userData = {\n color,\n opacity,\n scale,\n hover,\n };\n\n return corner;\n });\n};\n","import { GizmoOptionsFallback } from \"@lib/types\";\nimport {\n CanvasTexture,\n Mesh,\n MeshBasicMaterial,\n MeshBasicMaterialParameters,\n Sprite,\n SpriteMaterial,\n Vector3,\n} from \"three\";\nimport { roundedRectangleGeometry } from \"./roundedRectangleGeometry\";\nimport { setMapColumnOffset } from \"./axesMap\";\n\nexport const axesEdges = (\n options: GizmoOptionsFallback,\n texture: CanvasTexture,\n textureColumn: number\n) => {\n const { isSphere, edges } = options;\n\n if (!edges.enabled) return [];\n\n const { color, opacity, scale, hover, radius, smoothness } = edges;\n\n const geometry = isSphere\n ? null\n : roundedRectangleGeometry(radius, smoothness, 1.2, 0.25);\n\n const materialConfig: MeshBasicMaterialParameters = {\n transparent: true,\n opacity,\n };\n\n const positions = [\n 0, 1, 1, 0, -1, 1, 1, 0, 1, -1, 0, 1, 0, 1, -1, 0, -1, -1, 1, 0, -1, -1, 0,\n -1, 1, 1, 0, 1, -1, 0, -1, 1, 0, -1, -1, 0,\n ].map((val) => val * 0.925);\n\n const target = new Vector3();\n const defaultUp = new Vector3(0, 1, 0);\n return Array(positions.length / 3)\n .fill(0)\n .map<Mesh<any, MeshBasicMaterial> | Sprite>((_, i) => {\n if (isSphere) {\n const map = texture.clone();\n setMapColumnOffset(map, textureColumn);\n materialConfig.map = map;\n } else {\n materialConfig.color = color;\n }\n\n const edge = isSphere\n ? new Sprite(new SpriteMaterial(materialConfig))\n : new Mesh(geometry!, new MeshBasicMaterial(materialConfig));\n\n const i3 = i * 3;\n edge.position.set(positions[i3], positions[i3 + 1], positions[i3 + 2]);\n if (isSphere) edge.position.normalize().multiplyScalar(1.7);\n edge.scale.setScalar(scale);\n\n edge.up.copy(defaultUp);\n edge.lookAt(target.copy(edge.position).multiplyScalar(2));\n if (!isSphere && !edge.position.y) edge.rotation.z = Math.PI / 2;\n\n edge.renderOrder = 1;\n\n edge.userData = {\n color,\n opacity,\n scale,\n hover,\n };\n\n return edge;\n });\n};\n","import {\n\tBufferAttribute,\n\tBufferGeometry,\n\tFloat32BufferAttribute,\n\tInstancedBufferAttribute,\n\tInterleavedBuffer,\n\tInterleavedBufferAttribute,\n\tTriangleFanDrawMode,\n\tTriangleStripDrawMode,\n\tTrianglesDrawMode,\n\tVector3,\n} from 'three';\n\nfunction computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) {\n\n\tif ( ! MikkTSpace || ! MikkTSpace.isReady ) {\n\n\t\tthrow new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' );\n\n\t}\n\n\tif ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) {\n\n\t\tthrow new Error( 'BufferGeometryUtils: Tangents require \"position\", \"normal\", and \"uv\" attributes.' );\n\n\t}\n\n\tfunction getAttributeArray( attribute ) {\n\n\t\tif ( attribute.normalized || attribute.isInterleavedBufferAttribute ) {\n\n\t\t\tconst dstArray = new Float32Array( attribute.count * attribute.itemSize );\n\n\t\t\tfor ( let i = 0, j = 0; i < attribute.count; i ++ ) {\n\n\t\t\t\tdstArray[ j ++ ] = attribute.getX( i );\n\t\t\t\tdstArray[ j ++ ] = attribute.getY( i );\n\n\t\t\t\tif ( attribute.itemSize > 2 ) {\n\n\t\t\t\t\tdstArray[ j ++ ] = attribute.getZ( i );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn dstArray;\n\n\t\t}\n\n\t\tif ( attribute.array instanceof Float32Array ) {\n\n\t\t\treturn attribute.array;\n\n\t\t}\n\n\t\treturn new Float32Array( attribute.array );\n\n\t}\n\n\t// MikkTSpace algorithm requires non-indexed input.\n\n\tconst _geometry = geometry.index ? geometry.toNonIndexed() : geometry;\n\n\t// Compute vertex tangents.\n\n\tconst tangents = MikkTSpace.generateTangents(\n\n\t\tgetAttributeArray( _geometry.attributes.position ),\n\t\tgetAttributeArray( _geometry.attributes.normal ),\n\t\tgetAttributeArray( _geometry.attributes.uv )\n\n\t);\n\n\t// Texture coordinate convention of glTF differs from the apparent\n\t// default of the MikkTSpace library; .w component must be flipped.\n\n\tif ( negateSign ) {\n\n\t\tfor ( let i = 3; i < tangents.length; i += 4 ) {\n\n\t\t\ttangents[ i ] *= - 1;\n\n\t\t}\n\n\t}\n\n\t//\n\n\t_geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) );\n\n\tif ( geometry !== _geometry ) {\n\n\t\tgeometry.copy( _geometry );\n\n\t}\n\n\treturn geometry;\n\n}\n\n/**\n * @param {Array<BufferGeometry>} geometries\n * @param {Boolean} useGroups\n * @return {BufferGeometry}\n */\nfunction mergeGeometries( geometries, useGroups = false ) {\n\n\tconst isIndexed = geometries[ 0 ].index !== null;\n\n\tconst attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) );\n\tconst morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) );\n\n\tconst attributes = {};\n\tconst morphAttributes = {};\n\n\tconst morphTargetsRelative = geometries[ 0 ].morphTargetsRelative;\n\n\tconst mergedGeometry = new BufferGeometry();\n\n\tlet offset = 0;\n\n\tfor ( let i = 0; i < geometries.length; ++ i ) {\n\n\t\tconst geometry = geometries[ i ];\n\t\tlet attributesCount = 0;\n\n\t\t// ensure that all geometries are indexed, or none\n\n\t\tif ( isIndexed !== ( geometry.index !== null ) ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// gather attributes, exit early if they're different\n\n\t\tfor ( const name in geometry.attributes ) {\n\n\t\t\tif ( ! attributesUsed.has( name ) ) {\n\n\t\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure \"' + name + '\" attribute exists among all geometries, or in none of them.' );\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t\tif ( attributes[ name ] === undefined ) attributes[ name ] = [];\n\n\t\t\tattributes[ name ].push( geometry.attributes[ name ] );\n\n\t\t\tattributesCount ++;\n\n\t\t}\n\n\t\t// ensure geometries have the same number of attributes\n\n\t\tif ( attributesCount !== attributesUsed.size ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\t// gather morph attributes, exit early if they're different\n\n\t\tif ( morphTargetsRelative !== geometry.morphTargetsRelative ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tfor ( const name in geometry.morphAttributes ) {\n\n\t\t\tif ( ! morphAttributesUsed.has( name ) ) {\n\n\t\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' );\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t\tif ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = [];\n\n\t\t\tmorphAttributes[ name ].push( geometry.morphAttributes[ name ] );\n\n\t\t}\n\n\t\tif ( useGroups ) {\n\n\t\t\tlet count;\n\n\t\t\tif ( isIndexed ) {\n\n\t\t\t\tcount = geometry.index.count;\n\n\t\t\t} else if ( geometry.attributes.position !== undefined ) {\n\n\t\t\t\tcount = geometry.attributes.position.count;\n\n\t\t\t} else {\n\n\t\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' );\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t\tmergedGeometry.addGroup( offset, count, i );\n\n\t\t\toffset += count;\n\n\t\t}\n\n\t}\n\n\t// merge indices\n\n\tif ( isIndexed ) {\n\n\t\tlet indexOffset = 0;\n\t\tconst mergedIndex = [];\n\n\t\tfor ( let i = 0; i < geometries.length; ++ i ) {\n\n\t\t\tconst index = geometries[ i ].index;\n\n\t\t\tfor ( let j = 0; j < index.count; ++ j ) {\n\n\t\t\t\tmergedIndex.push( index.getX( j ) + indexOffset );\n\n\t\t\t}\n\n\t\t\tindexOffset += geometries[ i ].attributes.position.count;\n\n\t\t}\n\n\t\tmergedGeometry.setIndex( mergedIndex );\n\n\t}\n\n\t// merge attributes\n\n\tfor ( const name in attributes ) {\n\n\t\tconst mergedAttribute = mergeAttributes( attributes[ name ] );\n\n\t\tif ( ! mergedAttribute ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tmergedGeometry.setAttribute( name, mergedAttribute );\n\n\t}\n\n\t// merge morph attributes\n\n\tfor ( const name in morphAttributes ) {\n\n\t\tconst numMorphTargets = morphAttributes[ name ][ 0 ].length;\n\n\t\tif ( numMorphTargets === 0 ) break;\n\n\t\tmergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};\n\t\tmergedGeometry.morphAttributes[ name ] = [];\n\n\t\tfor ( let i = 0; i < numMorphTargets; ++ i ) {\n\n\t\t\tconst morphAttributesToMerge = [];\n\n\t\t\tfor ( let j = 0; j < morphAttributes[ name ].length; ++ j ) {\n\n\t\t\t\tmorphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] );\n\n\t\t\t}\n\n\t\t\tconst mergedMorphAttribute = mergeAttributes( morphAttributesToMerge );\n\n\t\t\tif ( ! mergedMorphAttribute ) {\n\n\t\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' );\n\t\t\t\treturn null;\n\n\t\t\t}\n\n\t\t\tmergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute );\n\n\t\t}\n\n\t}\n\n\treturn mergedGeometry;\n\n}\n\n/**\n * @param {Array<BufferAttribute>} attributes\n * @return {BufferAttribute}\n */\nfunction mergeAttributes( attributes ) {\n\n\tlet TypedArray;\n\tlet itemSize;\n\tlet normalized;\n\tlet gpuType = - 1;\n\tlet arrayLength = 0;\n\n\tfor ( let i = 0; i < attributes.length; ++ i ) {\n\n\t\tconst attribute = attributes[ i ];\n\n\t\tif ( TypedArray === undefined ) TypedArray = attribute.array.constructor;\n\t\tif ( TypedArray !== attribute.array.constructor ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tif ( itemSize === undefined ) itemSize = attribute.itemSize;\n\t\tif ( itemSize !== attribute.itemSize ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tif ( normalized === undefined ) normalized = attribute.normalized;\n\t\tif ( normalized !== attribute.normalized ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tif ( gpuType === - 1 ) gpuType = attribute.gpuType;\n\t\tif ( gpuType !== attribute.gpuType ) {\n\n\t\t\tconsole.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tarrayLength += attribute.count * itemSize;\n\n\t}\n\n\tconst array = new TypedArray( arrayLength );\n\tconst result = new BufferAttribute( array, itemSize, normalized );\n\tlet offset = 0;\n\n\tfor ( let i = 0; i < attributes.length; ++ i ) {\n\n\t\tconst attribute = attributes[ i ];\n\t\tif ( attribute.isInterleavedBufferAttribute ) {\n\n\t\t\tconst tupleOffset = offset / itemSize;\n\t\t\tfor ( let j = 0, l = attribute.count; j < l; j ++ ) {\n\n\t\t\t\tfor ( let c = 0; c < itemSize; c ++ ) {\n\n\t\t\t\t\tconst value = attribute.getComponent( j, c );\n\t\t\t\t\tresult.setComponent( j + tupleOffset, c, value );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tarray.set( attribute.array, offset );\n\n\t\t}\n\n\t\toffset += attribute.count * itemSize;\n\n\t}\n\n\tif ( gpuType !== undefined ) {\n\n\t\tresult.gpuType = gpuType;\n\n\t}\n\n\treturn result;\n\n}\n\n/**\n * @param {BufferAttribute} attribute\n * @return {BufferAttribute}\n */\nexport function deepCloneAttribute( attribute ) {\n\n\tif ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) {\n\n\t\treturn deinterleaveAttribute( attribute );\n\n\t}\n\n\tif ( attribute.isInstancedBufferAttribute ) {\n\n\t\treturn new InstancedBufferAttribute().copy( attribute );\n\n\t}\n\n\treturn new BufferAttribute().copy( attribute );\n\n}\n\n/**\n * @param {Array<BufferAttribute>} attributes\n * @return {Array<InterleavedBufferAttribute>}\n */\nfunction interleaveAttributes( attributes ) {\n\n\t// Interleaves the provided attributes into an InterleavedBuffer and returns\n\t// a set of InterleavedBufferAttributes for each attribute\n\tlet TypedArray;\n\tlet arrayLength = 0;\n\tlet stride = 0;\n\n\t// calculate the length and type of the interleavedBuffer\n\tfor ( let i = 0, l = attributes.length; i < l; ++ i ) {\n\n\t\tconst attribute = attributes[ i ];\n\n\t\tif ( TypedArray === undefined ) TypedArray = attribute.array.constructor;\n\t\tif ( TypedArray !== attribute.array.constructor ) {\n\n\t\t\tconsole.error( 'AttributeBuffers of different types cannot be interleaved' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\tarrayLength += attribute.array.length;\n\t\tstride += attribute.itemSize;\n\n\t}\n\n\t// Create the set of buffer attributes\n\tconst interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride );\n\tlet offset = 0;\n\tconst res = [];\n\tconst getters = [ 'getX', 'getY', 'getZ', 'getW' ];\n\tconst setters = [ 'setX', 'setY', 'setZ', 'setW' ];\n\n\tfor ( let j = 0, l = attributes.length; j < l; j ++ ) {\n\n\t\tconst attribute = attributes[ j ];\n\t\tconst itemSize = attribute.itemSize;\n\t\tconst count = attribute.count;\n\t\tconst iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized );\n\t\tres.push( iba );\n\n\t\toffset += itemSize;\n\n\t\t// Move the data for each attribute into the new interleavedBuffer\n\t\t// at the appropriate offset\n\t\tfor ( let c = 0; c < count; c ++ ) {\n\n\t\t\tfor ( let k = 0; k < itemSize; k ++ ) {\n\n\t\t\t\tiba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn res;\n\n}\n\n// returns a new, non-interleaved version of the provided attribute\nexport function deinterleaveAttribute( attribute ) {\n\n\tconst cons = attribute.data.array.constructor;\n\tconst count = attribute.count;\n\tconst itemSize = attribute.itemSize;\n\tconst normalized = attribute.normalized;\n\n\tconst array = new cons( count * itemSize );\n\tlet newAttribute;\n\tif ( attribute.isInstancedInterleavedBufferAttribute ) {\n\n\t\tnewAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute );\n\n\t} else {\n\n\t\tnewAttribute = new BufferAttribute( array, itemSize, normalized );\n\n\t}\n\n\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\tnewAttribute.setX( i, attribute.getX( i ) );\n\n\t\tif ( itemSize >= 2 ) {\n\n\t\t\tnewAttribute.setY( i, attribute.getY( i ) );\n\n\t\t}\n\n\t\tif ( itemSize >= 3 ) {\n\n\t\t\tnewAttribute.setZ( i, attribute.getZ( i ) );\n\n\t\t}\n\n\t\tif ( itemSize >= 4 ) {\n\n\t\t\tnewAttribute.setW( i, attribute.getW( i ) );\n\n\t\t}\n\n\t}\n\n\treturn newAttribute;\n\n}\n\n// deinterleaves all attributes on the geometry\nexport function deinterleaveGeometry( geometry ) {\n\n\tconst attributes = geometry.attributes;\n\tconst morphTargets = geometry.morphTargets;\n\tconst attrMap = new Map();\n\n\tfor ( const key in attributes ) {\n\n\t\tconst attr = attributes[ key ];\n\t\tif ( attr.isInterleavedBufferAttribute ) {\n\n\t\t\tif ( ! attrMap.has( attr ) ) {\n\n\t\t\t\tattrMap.set( attr, deinterleaveAttribute( attr ) );\n\n\t\t\t}\n\n\t\t\tattributes[ key ] = attrMap.get( attr );\n\n\t\t}\n\n\t}\n\n\tfor ( const key in morphTargets ) {\n\n\t\tconst attr = morphTargets[ key ];\n\t\tif ( attr.isInterleavedBufferAttribute ) {\n\n\t\t\tif ( ! attrMap.has( attr ) ) {\n\n\t\t\t\tattrMap.set( attr, deinterleaveAttribute( attr ) );\n\n\t\t\t}\n\n\t\t\tmorphTargets[ key ] = attrMap.get( attr );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * @param {BufferGeometry} geometry\n * @return {number}\n */\nfunction estimateBytesUsed( geometry ) {\n\n\t// Return the estimated memory used by this geometry in bytes\n\t// Calculate using itemSize, count, and BYTES_PER_ELEMENT to account\n\t// for InterleavedBufferAttributes.\n\tlet mem = 0;\n\tfor ( const name in geometry.attributes ) {\n\n\t\tconst attr = geometry.getAttribute( name );\n\t\tmem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;\n\n\t}\n\n\tconst indices = geometry.getIndex();\n\tmem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;\n\treturn mem;\n\n}\n\n/**\n * @param {BufferGeometry} geometry\n * @param {number} tolerance\n * @return {BufferGeometry}\n */\nfunction mergeVertices( geometry, tolerance = 1e-4 ) {\n\n\ttolerance = Math.max( tolerance, Number.EPSILON );\n\n\t// Generate an index buffer if the geometry doesn't have one, or optimize it\n\t// if it's already available.\n\tconst hashToIndex = {};\n\tconst indices = geometry.getIndex();\n\tconst positions = geometry.getAttribute( 'position' );\n\tconst vertexCount = indices ? indices.count : positi