miijs
Version:
Read, Edit, Write, and make Special Miis from a Wiimote binary file or 3DS QR Code to a binary file or QR code
1,567 lines (1,441 loc) • 183 kB
JavaScript
// @ts-check
'use strict';
/*!
* Bindings for FFL, a Mii renderer, in JavaScript.
* https://github.com/ariankordi/FFL.js
* @author Arian Kordi <https://github.com/ariankordi>
*/
// ------------------ ESM imports, uncomment if you use ESM ------------------
/*
// Also see the bottom of the script for corresponding exports.
import * as THREE from 'three';
// import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.167.0/+esm';
import * as _Import from './struct-fu.js';
*/
// Hack to get library globals recognized throughout the file (remove for ESM).
/**
// * @ typedef {import('./struct-fu')} _
// * @ typedef {import('three')} THREE
*/
/* eslint-disable no-self-assign -- Get TypeScript to identify global imports. */
const _=require("./struct-fu.js");
const THREE=require("three");
globalThis._ = /** @type {_} */ (/** @type {*} */ (globalThis)._);
globalThis.THREE = /** @type {THREE} */ (/** @type {*} */ (globalThis).THREE);
// NOTeslint-disable-next-line @stylistic/max-statements-per-line -- Hack to use either UMD or browser ESM import.
// let _ = globalThis._; _ = (!_) ? _Import : _; // Uncomment for ESM
/* eslint-enable no-self-assign -- Get TypeScript to identify global imports. */
/* globals _ THREE -- Global dependencies. */
// // ---------------------------------------------------------------------
// // Emscripten Types
// // ---------------------------------------------------------------------
// TODO PATH: src/ModuleType.js
/**
* Emscripten "Module" type.
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c03bddd4d3c7774d00fa256a9e165d68c7534ccc/types/emscripten/index.d.ts#L26
* @typedef {Object} Module
* @property {function(): void} onRuntimeInitialized
* @property {function(object): void} destroy
* @property {boolean|null} calledRun
* // USE_TYPED_ARRAYS == 2
* @property {Int8Array} HEAP8
* @property {Uint8Array} HEAPU8
* @property {Uint16Array} HEAPU16
* @property {Uint32Array} HEAPU32
* @property {Float32Array} HEAPF32
* Runtime methods:
* @property {function(number): number} _malloc
* @property {function(number): void} _free
* @property {function((...args: *[]) => *, string=): number} addFunction
* @property {function(number): void} removeFunction
*
* ------------------------------- FFL Bindings -------------------------------
* @property {function(number, number, number, number): *} _FFLInitCharModelCPUStepWithCallback
* @property {function(number, number, number): *} _FFLInitCharModelCPUStep
* @property {function(number): *} _FFLDeleteCharModel
* @property {function(number): *} _FFLGetDrawParamOpaFaceline
* @property {function(number): *} _FFLGetDrawParamOpaBeard
* @property {function(number): *} _FFLGetDrawParamOpaNose
* @property {function(number): *} _FFLGetDrawParamOpaForehead
* @property {function(number): *} _FFLGetDrawParamOpaHair
* @property {function(number): *} _FFLGetDrawParamOpaCap
* @property {function(number): *} _FFLGetDrawParamXluMask
* @property {function(number): *} _FFLGetDrawParamXluNoseLine
* @property {function(number): *} _FFLGetDrawParamXluGlass
* @property {function(number, number): *} _FFLSetExpression
* @property {function(number): *} _FFLGetExpression
* @property {function(number, number): *} _FFLSetViewModelType
* @property {function(number, number): *} _FFLGetBoundingBox
* @property {function(number, number): *} _FFLIsAvailableExpression
* @property {function(number, number): *} _FFLSetCoordinate
* @property {function(number): *} _FFLSetScale
* @property {function(number, number, number, number): *} _FFLiGetRandomCharInfo
* @property {function(number, number): *} _FFLpGetStoreDataFromCharInfo
* @property {function(number, number): *} _FFLpGetCharInfoFromStoreData
* @property {function(number, number): *} _FFLpGetCharInfoFromMiiDataOfficialRFL
* @property {function(number, number, number, number, boolean): *} _FFLGetAdditionalInfo
* @property {function(number, number): *} _FFLInitRes
* @property {function(): *} _FFLInitResGPUStep
* @property {function(): *} _FFLExit
* @property {function(): *} _FFLIsAvailable
* @property {function(number, number): *} _FFLGetFavoriteColor
* @property {function(number): *} _FFLSetLinearGammaMode
* @property {function(number, number): *} _FFLGetFacelineColor
* @property {function(boolean): *} _FFLSetTextureFlipY
* @property {function(boolean): *} _FFLSetNormalIsSnorm8_8_8_8
* @property {function(boolean): *} _FFLSetFrontCullForFlipX
* @property {function(number): *} _FFLSetTextureCallback
* @property {function(number): *} _FFLiDeleteTextureTempObject
* @property {function(number, number, number): *} _FFLiDeleteTempObjectMaskTextures
* @property {function(number, number, number): *} _FFLiDeleteTempObjectFacelineTexture
* @property {function(number): *} _FFLiiGetEyeRotateOffset
* @property {function(number): *} _FFLiiGetEyebrowRotateOffset
* @property {function(number): *} _FFLiInvalidateTempObjectFacelineTexture
* @property {function(number): *} _FFLiInvalidatePartsTextures
* @property {function(number): *} _FFLiInvalidateRawMask
* @property {function(number, boolean): *} _FFLiVerifyCharInfoWithReason
* @property {function(): void} _exit
*/
// // ---------------------------------------------------------------------
// // Enum Definitions
// // ---------------------------------------------------------------------
// TODO PATH: src/Enums.js
/**
* Uses FFL decomp enum rather than real FFL enum.
* Reference: https://github.com/aboood40091/ffl/blob/master/include/nn/ffl/FFLResult.h
* @enum {number}
*/
const FFLResult = {
OK: 0,
ERROR: 1,
HDB_EMPTY: 2,
FILE_INVALID: 3,
MANAGER_NOT_CONSTRUCT: 4,
FILE_LOAD_ERROR: 5,
// : 6,
FILE_SAVE_ERROR: 7,
// : 8,
RES_FS_ERROR: 9,
ODB_EMPTY: 10,
// : 11,
OUT_OF_MEMORY: 12,
// : 13,
// : 14,
// : 15,
// : 16,
UNKNOWN_17: 17,
FS_ERROR: 18,
FS_NOT_FOUND: 19,
MAX: 20
};
/** @enum {number} */
const FFLiShapeType = {
OPA_BEARD: 0,
OPA_FACELINE: 1,
OPA_HAIR_NORMAL: 2,
OPA_FOREHEAD_NORMAL: 3,
XLU_MASK: 4,
XLU_NOSELINE: 5,
OPA_NOSE: 6,
OPA_HAT_NORMAL: 7,
XLU_GLASS: 8,
OPA_HAIR_CAP: 9,
OPA_FOREHEAD_CAP: 10,
OPA_HAT_CAP: 11,
MAX: 12
};
/** @enum {number} */
const FFLAttributeBufferType = {
POSITION: 0,
TEXCOORD: 1,
NORMAL: 2,
TANGENT: 3,
COLOR: 4,
MAX: 5
};
/** @enum {number} */
const FFLCullMode = {
NONE: 0,
BACK: 1,
FRONT: 2,
MAX: 3
};
/** @enum {number} */
const FFLModulateMode = {
/** No Texture, Has Color (R) */
CONSTANT: 0,
/** Has Texture, No Color */
TEXTURE_DIRECT: 1,
/** Has Texture, Has Color (R + G + B) */
RGB_LAYERED: 2,
/** Has Texture, Has Color (R) */
ALPHA: 3,
/** Has Texture, Has Color (R) */
LUMINANCE_ALPHA: 4,
/** Has Texture, Has Color (R) */
ALPHA_OPA: 5
};
/** @enum {number} */
const FFLModulateType = {
SHAPE_FACELINE: 0,
SHAPE_BEARD: 1,
SHAPE_NOSE: 2,
SHAPE_FOREHEAD: 3,
SHAPE_HAIR: 4,
SHAPE_CAP: 5,
SHAPE_MASK: 6,
SHAPE_NOSELINE: 7,
SHAPE_GLASS: 8,
MUSTACHE: 9,
MOUTH: 10,
EYEBROW: 11,
EYE: 12,
MOLE: 13,
FACE_MAKE: 14,
FACE_LINE: 15,
FACE_BEARD: 16,
FILL: 17,
SHAPE_MAX: 9
};
/** @enum {number} */
const FFLResourceType = {
MIDDLE: 0,
HIGH: 1,
MAX: 2
};
/**
* Reference: https://github.com/ariankordi/ffl/blob/nsmbu-win-port-linux64/include/nn/ffl/FFLExpression.h
* @enum {number}
*/
const FFLExpression = {
NORMAL: 0,
SMILE: 1,
ANGER: 2,
/** Primary name for expression 3. */
SORROW: 3,
PUZZLED: 3,
/** Primary name for expression 4. */
SURPRISE: 4,
SURPRISED: 4,
BLINK: 5,
OPEN_MOUTH: 6,
/** Primary name for expression 7. */
SMILE_OPEN_MOUTH: 7,
HAPPY: 7,
ANGER_OPEN_MOUTH: 8,
SORROW_OPEN_MOUTH: 9,
SURPRISE_OPEN_MOUTH: 10,
BLINK_OPEN_MOUTH: 11,
WINK_LEFT: 12,
WINK_RIGHT: 13,
WINK_LEFT_OPEN_MOUTH: 14,
WINK_RIGHT_OPEN_MOUTH: 15,
/** Primary name for expression 16. */
LIKE_WINK_LEFT: 16,
LIKE: 16,
LIKE_WINK_RIGHT: 17,
FRUSTRATED: 18,
// Additional expressions from AFL.
// Enum names are completely made up.
BORED: 19,
BORED_OPEN_MOUTH: 20,
SIGH_MOUTH_STRAIGHT: 21,
SIGH: 22,
DISGUSTED_MOUTH_STRAIGHT: 23,
DISGUSTED: 24,
LOVE: 25,
LOVE_OPEN_MOUTH: 26,
DETERMINED_MOUTH_STRAIGHT: 27,
DETERMINED: 28,
CRY_MOUTH_STRAIGHT: 29,
CRY: 30,
BIG_SMILE_MOUTH_STRAIGHT: 31,
BIG_SMILE: 32,
CHEEKY: 33,
CHEEKY_DUPLICATE: 34,
JOJO_EYES_FUNNY_MOUTH: 35,
JOJO_EYES_FUNNY_MOUTH_OPEN: 36,
SMUG: 37,
SMUG_OPEN_MOUTH: 38,
RESOLVE: 39,
RESOLVE_OPEN_MOUTH: 40,
UNBELIEVABLE: 41,
UNBELIEVABLE_DUPLICATE: 42,
CUNNING: 43,
CUNNING_DUPLICATE: 44,
RASPBERRY: 45,
RASPBERRY_DUPLICATE: 46,
INNOCENT: 47,
INNOCENT_DUPLICATE: 48,
CAT: 49,
CAT_DUPLICATE: 50,
DOG: 51,
DOG_DUPLICATE: 52,
TASTY: 53,
TASTY_DUPLICATE: 54,
MONEY_MOUTH_STRAIGHT: 55,
MONEY: 56,
SPIRAL_MOUTH_STRAIGHT: 57,
CONFUSED: 58,
CHEERFUL_MOUTH_STRAIGHT: 59,
CHEERFUL: 60,
BLANK_61: 61,
BLANK_62: 62,
GRUMBLE_MOUTH_STRAIGHT: 63,
GRUMBLE: 64,
MOVED_MOUTH_STRAIGHT: 65,
MOVED: 66,
SINGING_MOUTH_SMALL: 67,
SINGING: 68,
STUNNED: 69,
MAX: 70
};
/**
* Model flags modify how the head model is created. These are
* used in the `modelFlag` property of {@link FFLCharModelDesc}.
* @enum {number}
*/
const FFLModelFlag = {
/** Default model setting. */
NORMAL: 1 << 0,
/** Uses a variant of hair designed for hats. */
HAT: 1 << 1,
/** Discards hair from the model, used for helmets and similar headwear. */
FACE_ONLY: 1 << 2,
/** Limits Z depth on the nose, useful for helmets and similar headwear. */
FLATTEN_NOSE: 1 << 3,
/** Enables the model's expression flag to use expressions beyond 32. */
NEW_EXPRESSIONS: 1 << 4,
/**
* This flag only generates new textures when initializing a CharModel
* but does not initialize shapes.
* **Note:** This means you cannot use DrawOpa/Xlu when this is set.
*/
NEW_MASK_ONLY: 1 << 5
};
// // ---------------------------------------------------------------------
// // Struct Definitions (struct-fu)
// // ---------------------------------------------------------------------
// TODO PATH: src/Structs.js
// Mostly leading up to FFLDrawParam.
/** Mirror for {@link _.uint32le} to indicate a pointer. */
const _uintptr = _.uint32le;
/**
* @typedef {Object} FFLAttributeBuffer
* @property {number} size
* @property {number} stride
* @property {number} ptr
*/
/** @type {import('./struct-fu').StructInstance<FFLAttributeBuffer>} */
const FFLAttributeBuffer = _.struct([
_.uint32le('size'),
_.uint32le('stride'),
_uintptr('ptr')
]);
/**
* @typedef {Object} FFLAttributeBufferParam
* @property {Array<FFLAttributeBuffer>} attributeBuffers
*/
/** @type {import('./struct-fu').StructInstance<FFLAttributeBufferParam>} */
const FFLAttributeBufferParam = _.struct([
_.struct('attributeBuffers', [FFLAttributeBuffer], 5)
]);
/**
* @typedef {Object} FFLPrimitiveParam
* @property {number} primitiveType
* @property {number} indexCount
* @property {number} pAdjustMatrix
* @property {number} pIndexBuffer
*/
/** @type {import('./struct-fu').StructInstance<FFLPrimitiveParam>} */
const FFLPrimitiveParam = _.struct([
_.uint32le('primitiveType'),
_.uint32le('indexCount'),
_uintptr('pAdjustMatrix'), // TODO: Not uintptr in 64-bit.
_uintptr('pIndexBuffer')
]);
/**
* @typedef {Object} FFLColor
* @property {number} r
* @property {number} g
* @property {number} b
* @property {number} a
*/
/** @type {import('./struct-fu').StructInstance<FFLColor>} */
const FFLColor = _.struct([
_.float32le('r'),
_.float32le('g'),
_.float32le('b'),
_.float32le('a')
]);
/**
* @typedef {Object} FFLVec3
* @property {number} x
* @property {number} y
* @property {number} z
*/
/** @type {import('./struct-fu').StructInstance<FFLVec3>} */
const FFLVec3 = _.struct([
_.float32le('x'),
_.float32le('y'),
_.float32le('z')
]);
/**
* @typedef {Object} FFLModulateParam
* @property {FFLModulateMode} mode
* @property {FFLModulateType} type
* @property {number} pColorR - Pointer to FFLColor
* @property {number} pColorG - Pointer to FFLColor
* @property {number} pColorB - Pointer to FFLColor
* @property {number} pTexture2D
*/
/** @type {import('./struct-fu').StructInstance<FFLModulateParam>} */
const FFLModulateParam = _.struct([
_.uint32le('mode'), // enum FFLModulateMode
_.uint32le('type'), // enum FFLModulateType
_uintptr('pColorR'),
_uintptr('pColorG'),
_uintptr('pColorB'),
_uintptr('pTexture2D')
]);
/**
* @typedef {Object} FFLDrawParam
* @property {FFLAttributeBufferParam} attributeBufferParam
* @property {FFLModulateParam} modulateParam
* @property {FFLCullMode} cullMode
* @property {FFLPrimitiveParam} primitiveParam
*/
/** @type {import('./struct-fu').StructInstance<FFLDrawParam>} */
const FFLDrawParam = _.struct([
_.struct('attributeBufferParam', [FFLAttributeBufferParam]),
_.struct('modulateParam', [FFLModulateParam]),
_.uint32le('cullMode'),
_.struct('primitiveParam', [FFLPrimitiveParam])
]);
// ---------------------- Begin FFLiCharInfo Definition ----------------------
// TODO PATH: src/StructFFLiCharModel.js
/**
* @typedef {Object} FFLCreateID
* @property {Array<number>} data
*/
/** @type {import('./struct-fu').StructInstance<FFLCreateID>} */
const FFLCreateID = _.struct([
_.uint8('data', 10)
]);
/**
* @typedef {Object} FFLiCharInfo_faceline
* @property {number} type
* @property {number} color
* @property {number} texture
* @property {number} make
*/
/**
* @typedef {Object} FFLiCharInfo_hair
* @property {number} type
* @property {number} color
* @property {number} flip
*/
/**
* @typedef {Object} FFLiCharInfo_eye
* @property {number} type
* @property {number} color
* @property {number} scale
* @property {number} aspect
* @property {number} rotate
* @property {number} x
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_eyebrow
* @property {number} type
* @property {number} color
* @property {number} scale
* @property {number} aspect
* @property {number} rotate
* @property {number} x
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_nose
* @property {number} type
* @property {number} scale
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_mouth
* @property {number} type
* @property {number} color
* @property {number} scale
* @property {number} aspect
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_beard
* @property {number} mustache
* @property {number} type
* @property {number} color
* @property {number} scale
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_glass
* @property {number} type
* @property {number} color
* @property {number} scale
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_mole
* @property {number} type
* @property {number} scale
* @property {number} x
* @property {number} y
*/
/**
* @typedef {Object} FFLiCharInfo_body
* @property {number} height
* @property {number} build
*/
/**
* @typedef {Object} FFLiCharInfo_personal
* @property {string} name
* @property {string} creator
* @property {number} gender
* @property {number} birthMonth
* @property {number} birthDay
* @property {number} favoriteColor
* @property {number} favorite
* @property {number} copyable
* @property {number} ngWord
* @property {number} localonly
* @property {number} regionMove
* @property {number} fontRegion
* @property {number} roomIndex
* @property {number} positionInRoom
* @property {number} birthPlatform
*/
/**
* @typedef {Object} FFLiCharInfo
* @property {number} miiVersion
* @property {FFLiCharInfo_faceline} faceline
* @property {FFLiCharInfo_hair} hair
* @property {FFLiCharInfo_eye} eye
* @property {FFLiCharInfo_eyebrow} eyebrow
* @property {FFLiCharInfo_nose} nose
* @property {FFLiCharInfo_mouth} mouth
* @property {FFLiCharInfo_beard} beard
* @property {FFLiCharInfo_glass} glass
* @property {FFLiCharInfo_mole} mole
* @property {FFLiCharInfo_body} body
* @property {FFLiCharInfo_personal} personal
* @property {FFLCreateID} createID
* @property {number} padding_0
* @property {number} authorType
* @property {Array<number>} authorID
*/
/** @type {import('./struct-fu').StructInstance<FFLiCharInfo>} */
const FFLiCharInfo = _.struct([
_.int32le('miiVersion'),
_.struct('faceline', [_.int32le('type'), _.int32le('color'),
_.int32le('texture'), _.int32le('make')]),
_.struct('hair', [_.int32le('type'), _.int32le('color'), _.int32le('flip')]),
_.struct('eye', [_.int32le('type'), _.int32le('color'), _.int32le('scale'),
_.int32le('aspect'), _.int32le('rotate'),
_.int32le('x'), _.int32le('y')]),
_.struct('eyebrow', [_.int32le('type'), _.int32le('color'),
_.int32le('scale'), _.int32le('aspect'),
_.int32le('rotate'), _.int32le('x'),
_.int32le('y')]),
_.struct('nose', [_.int32le('type'), _.int32le('scale'),
_.int32le('y')]),
_.struct('mouth', [_.int32le('type'), _.int32le('color'),
_.int32le('scale'), _.int32le('aspect'),
_.int32le('y')]),
_.struct('beard', [_.int32le('mustache'), _.int32le('type'),
_.int32le('color'), _.int32le('scale'),
_.int32le('y')]),
_.struct('glass', [_.int32le('type'), _.int32le('color'),
_.int32le('scale'), _.int32le('y')]),
_.struct('mole', [_.int32le('type'), _.int32le('scale'),
_.int32le('x'), _.int32le('y')]),
_.struct('body', [_.int32le('height'), _.int32le('build')]),
_.struct('personal', [
_.char16le('name', 22),
_.char16le('creator', 22),
_.int32le('gender'),
_.int32le('birthMonth'),
_.int32le('birthDay'),
_.int32le('favoriteColor'),
_.uint8('favorite'),
_.uint8('copyable'),
_.uint8('ngWord'),
_.uint8('localonly'),
_.int32le('regionMove'),
_.int32le('fontRegion'),
_.int32le('roomIndex'),
_.int32le('positionInRoom'),
_.int32le('birthPlatform')
]),
_.struct('createID', [FFLCreateID]),
_.uint16le('padding_0'),
_.int32le('authorType'),
_.uint8('authorID', 8) // stub
]);
/**
* Size of FFLStoreData, a structure not included currently.
* @public
*/
/** sizeof(FFLStoreData) */
const FFLStoreData_size = 96;
// ---------------------- Common Color Mask Definitions ----------------------
/** @package */
const commonColorEnableMask = (1 << 31);
/**
* Applies (unofficial) mask: FFLI_NN_MII_COMMON_COLOR_ENABLE_MASK
* to a common color index to indicate to FFL which color table it should use.
* @param {number} color - The color index to flag.
* @returns {number} The flagged color index to use in FFLiCharinfo.
*/
const commonColorMask = color => color | commonColorEnableMask;
/**
* Removes (unofficial) mask: FFLI_NN_MII_COMMON_COLOR_ENABLE_MASK
* to a common color index to reveal the original common color index.
* @param {number} color - The flagged color index.
* @returns {number} The original color index before flagging.
*/
const commonColorUnmask = color => (color & ~commonColorEnableMask) === 0
// Only unmask color if the mask is enabled.
? color
: color & ~commonColorEnableMask;
// --------------------- Begin FFLiCharModel Definitions ---------------------
/**
* @typedef {Object} FFLAdditionalInfo
* @property {string} name
* @property {string} creator
* @property {FFLCreateID} createID
* @property {FFLColor} skinColor
* @property {number} flags
* @property {number} facelineType
* @property {number} hairType
*/
/** @type {import('./struct-fu').StructInstance<FFLAdditionalInfo>} */
const FFLAdditionalInfo = _.struct([
_.char16le('name', 22),
_.char16le('creator', 22),
_.struct('createID', [FFLCreateID]),
_.byte('_padding0', 2), // alignment
_.struct('skinColor', [FFLColor]),
_.uint32le('flags'),
// _.ubitLE('hairFlip', 1),
// _.ubitLE('fontRegion', 2),
// _.ubitLE('ngWord', 1),
// _.ubitLE('build', 7),
// _.ubitLE('height', 7),
// _.ubitLE('favoriteColor', 4),
// _.ubitLE('birthDay', 5),
// _.ubitLE('birthMonth', 4),
// _.ubitLE('gender', 1),
_.uint8('facelineType'),
_.uint8('hairType'),
_.byte('_padding1', 2) // alignment
]);
const FFLiRenderTexture = _.struct([
// STUB: four pointers in one field
_uintptr('pTexture2DRenderBufferColorTargetDepthTarget', 4)
]);
/**
* @typedef {Object} FFLiFacelineTextureTempObject
* @property {number} pTextureFaceLine
* @property {FFLDrawParam} drawParamFaceLine
* @property {number} pTextureFaceMake
* @property {FFLDrawParam} drawParamFaceMake
* @property {number} pTextureFaceBeard
* @property {FFLDrawParam} drawParamFaceBeard
* @property {Array<number>} pRenderTextureCompressorParam
*/
/** @type {import('./struct-fu').StructInstance<FFLiFacelineTextureTempObject>} */
const FFLiFacelineTextureTempObject = _.struct([
_uintptr('pTextureFaceLine'),
_.struct('drawParamFaceLine', [FFLDrawParam]),
_uintptr('pTextureFaceMake'),
_.struct('drawParamFaceMake', [FFLDrawParam]),
_uintptr('pTextureFaceBeard'),
_.struct('drawParamFaceBeard', [FFLDrawParam]),
_uintptr('pRenderTextureCompressorParam', 2) // stub
]);
/**
* @typedef {Object} FFLiRawMaskDrawParam
* @property {Array<FFLDrawParam>} drawParamRawMaskPartsEye - 2
* @property {Array<FFLDrawParam>} drawParamRawMaskPartsEyebrow - 2
* @property {FFLDrawParam} drawParamRawMaskPartsMouth
* @property {Array<FFLDrawParam>} drawParamRawMaskPartsMustache - 2
* @property {FFLDrawParam} drawParamRawMaskPartsMole
* @property {FFLDrawParam} drawParamRawMaskPartsFill
*/
/** @type {import('./struct-fu').StructInstance<FFLiRawMaskDrawParam>} */
const FFLiRawMaskDrawParam = _.struct([
_.struct('drawParamRawMaskPartsEye', [FFLDrawParam], 2),
_.struct('drawParamRawMaskPartsEyebrow', [FFLDrawParam], 2),
_.struct('drawParamRawMaskPartsMouth', [FFLDrawParam]),
_.struct('drawParamRawMaskPartsMustache', [FFLDrawParam], 2),
_.struct('drawParamRawMaskPartsMole', [FFLDrawParam]),
_.struct('drawParamRawMaskPartsFill', [FFLDrawParam])
]);
/**
* @typedef {Object} FFLiMaskTexturesTempObject
* @property {Array<number>} partsTextures
* @property {Array<number>} pRawMaskDrawParam
* @property {Uint8Array} _remaining
*/
/** @type {import('./struct-fu').StructInstance<FFLiMaskTexturesTempObject>} */
const FFLiMaskTexturesTempObject = _.struct([
_.uint8('partsTextures', 0x154),
_uintptr('pRawMaskDrawParam', FFLExpression.MAX),
_.byte('_remaining', 0x388 - 620) // stub
]);
/**
* @typedef {Object} FFLiTextureTempObject
* @property {FFLiMaskTexturesTempObject} maskTextures
* @property {FFLiFacelineTextureTempObject} facelineTexture
*/
/** @type {import('./struct-fu').StructInstance<FFLiTextureTempObject>} */
const FFLiTextureTempObject = _.struct([
_.struct('maskTextures', [FFLiMaskTexturesTempObject]),
_.struct('facelineTexture', [FFLiFacelineTextureTempObject])
]);
/**
* @typedef {Object} FFLiMaskTextures
* @property {Array<number>} pRenderTextures
*/
/** @type {import('./struct-fu').StructInstance<FFLiMaskTextures>} */
const FFLiMaskTextures = _.struct([
_uintptr('pRenderTextures', FFLExpression.MAX)
]);
/** @package */
const FFL_RESOLUTION_MASK = 0x3fffffff;
/**
* @typedef {Object} FFLCharModelDesc
* @property {number} resolution - Texture resolution for faceline/mask. It's recommended to only use powers of two.
* @property {Uint32Array} allExpressionFlag - Expression flag, created by {@link makeExpressionFlag}
* @property {FFLModelFlag} modelFlag
* @property {FFLResourceType} resourceType
*/
/** @type {import('./struct-fu').StructInstance<FFLCharModelDesc>} */
const FFLCharModelDesc = _.struct([
_.uint32le('resolution'),
_.uint32le('allExpressionFlag', 3),
_.uint32le('modelFlag'),
_.uint32le('resourceType')
]);
/**
* Static default for FFLCharModelDesc.
* @type {FFLCharModelDesc}
* @readonly
* @public
*/
const FFLCharModelDescDefault = {
/** Typical default. */
resolution: 512,
/** Normal expression. */
allExpressionFlag: new Uint32Array([1, 0, 0]),
modelFlag: FFLModelFlag.NORMAL,
/** Default resource type. */
resourceType: FFLResourceType.HIGH
};
/** @typedef {FFLCharModelDesc|Array<FFLExpression>|FFLExpression|Uint32Array|null} CharModelDescOrExpressionFlag */
/**
* @typedef {Object<string, FFLVec3>} FFLBoundingBox
* @property {FFLVec3} min
* @property {FFLVec3} max
*/
/** @type {import('./struct-fu').StructInstance<FFLBoundingBox>} */
const FFLBoundingBox = _.struct([
_.struct('min', [FFLVec3]),
_.struct('max', [FFLVec3])
]);
/**
* @typedef {Object<string, FFLVec3>} FFLPartsTransform
* @property {FFLVec3} hatTranslate
* @property {FFLVec3} headFrontRotate
* @property {FFLVec3} headFrontTranslate
* @property {FFLVec3} headSideRotate
* @property {FFLVec3} headSideTranslate
* @property {FFLVec3} headTopRotate
* @property {FFLVec3} headTopTranslate
*/
/** @type {import('./struct-fu').StructInstance<FFLPartsTransform>} */
const FFLPartsTransform = _.struct([
_.struct('hatTranslate', [FFLVec3]),
_.struct('headFrontRotate', [FFLVec3]),
_.struct('headFrontTranslate', [FFLVec3]),
_.struct('headSideRotate', [FFLVec3]),
_.struct('headSideTranslate', [FFLVec3]),
_.struct('headTopRotate', [FFLVec3]),
_.struct('headTopTranslate', [FFLVec3])
]);
/**
* PartsTransform with THREE.Vector3 type.
* @typedef {Object<string, import('three').Vector3>} PartsTransform
* @property {import('three').Vector3} hatTranslate
* @property {import('three').Vector3} headFrontRotate
* @property {import('three').Vector3} headFrontTranslate
* @property {import('three').Vector3} headSideRotate
* @property {import('three').Vector3} headSideTranslate
* @property {import('three').Vector3} headTopRotate
* @property {import('three').Vector3} headTopTranslate
*/
/**
* Internal representation within FFL for the created CharModel.
* @typedef {Object} FFLiCharModel
* @property {FFLiCharInfo} charInfo
* @property {FFLCharModelDesc} charModelDesc
* @property {FFLExpression} expression
* @property {number} pTextureTempObject
* @property {Array<FFLDrawParam>} drawParam
* @property {Array<number>} pShapeData
* @property {Array<Object>} facelineRenderTexture
* @property {Array<number>} pCapGlassNoselineTextures
* @property {FFLiMaskTextures} maskTextures
* @property {Array<FFLVec3>} beardHairFaceCenterPos
* @property {FFLPartsTransform} partsTransform
* @property {number} modelType - FFLModelType
* @property {Array<FFLBoundingBox>} boundingBox
*/
/** @type {import('./struct-fu').StructInstance<FFLiCharModel>} */
const FFLiCharModel = _.struct([
_.struct('charInfo', [FFLiCharInfo]),
_.struct('charModelDesc', [FFLCharModelDesc]),
_.uint32le('expression'), // enum FFLExpression
_uintptr('pTextureTempObject'), // stub
_.struct('drawParam', [FFLDrawParam], FFLiShapeType.MAX),
_uintptr('pShapeData', FFLiShapeType.MAX),
_.struct('facelineRenderTexture', [FFLiRenderTexture]),
_uintptr('pCapGlassNoselineTextures', 3),
_.struct('maskTextures', [FFLiMaskTextures]),
_.struct('beardHairFaceCenterPos', [FFLVec3], 3),
_.struct('partsTransform', [FFLPartsTransform]),
_.uint32le('modelType'), // enum FFLModelType
// FFLBoundingBox[FFL_MODEL_TYPE_MAX = 3]
_.struct('boundingBox', [FFLBoundingBox], 3)
]);
/** @enum {number} */
const FFLDataSource = {
OFFICIAL: 0,
DEFAULT: 1,
MIDDLE_DB: 2,
STORE_DATA_OFFICIAL: 3,
STORE_DATA: 4,
BUFFER: 5,
DIRECT_POINTER: 6
};
/**
* @typedef {Object} FFLCharModelSource
* @property {FFLDataSource} dataSource
* @property {number} pBuffer
* @property {number} index - Only for default, official, MiddleDB; unneeded for raw data
*/
/** @type {import('./struct-fu').StructInstance<FFLCharModelSource>} */
const FFLCharModelSource = _.struct([
_.uint32le('dataSource'),
_uintptr('pBuffer'),
_.uint16le('index')
]);
// The enums below are only for FFLiGetRandomCharInfo.
// Hence, why each one has a value called ALL.
/** @enum {number} */
const FFLGender = {
MALE: 0,
FEMALE: 1,
ALL: 2
};
/** @enum {number} */
const FFLAge = {
CHILD: 0,
ADULT: 1,
ELDER: 2,
ALL: 3
};
/** @enum {number} */
const FFLRace = {
BLACK: 0,
WHITE: 1,
ASIAN: 2,
ALL: 3
};
/**
* @typedef {Object} FFLResourceDesc
* @property {Array<number>} pData
* @property {Array<number>} size
*/
/** @type {import('./struct-fu').StructInstance<FFLResourceDesc>} */
const FFLResourceDesc = _.struct([
_uintptr('pData', FFLResourceType.MAX),
_.uint32le('size', FFLResourceType.MAX)
]);
// // ---------------------------------------------------------------------
// // Texture Management
// // ---------------------------------------------------------------------
// ------------------------- Texture Related Structs -------------------------
/** @enum {number} */
const FFLTextureFormat = {
R8_UNORM: 0,
R8_G8_UNORM: 1,
R8_G8_B8_A8_UNORM: 2,
MAX: 3
};
/**
* @typedef {Object} FFLTextureInfo
* @property {number} width
* @property {number} height
* @property {number} mipCount
* @property {FFLTextureFormat} format
* @property {number} isGX2Tiled
* @property {number} imageSize
* @property {number} imagePtr
* @property {number} mipSize
* @property {number} mipPtr
* @property {Array<number>} mipLevelOffset
*/
/** @type {import('./struct-fu').StructInstance<FFLTextureInfo>} */
const FFLTextureInfo = _.struct([
_.uint16le('width'),
_.uint16le('height'),
_.uint8('mipCount'),
_.uint8('format'),
_.uint8('isGX2Tiled'),
_.byte('_padding', 1),
_.uint32le('imageSize'),
_uintptr('imagePtr'),
_.uint32le('mipSize'),
_uintptr('mipPtr'),
_.uint32le('mipLevelOffset', 13)
]);
const FFLTextureCallback = _.struct([
_uintptr('pObj'),
_.uint8('useOriginalTileMode'),
_.byte('_padding', 3), // alignment
_uintptr('pCreateFunc'),
_uintptr('pDeleteFunc')
]);
// TODO PATH: src/TextureManager.js
// ------------------------ Class: TextureManager -----------------------------
/**
* Manages THREE.Texture objects created via FFL.
* Must be instantiated after FFL is fully initialized.
*/
class TextureManager {
/**
* Constructs the TextureManager. This MUST be created after initializing FFL.
* @param {Module} module - The Emscripten module.
* @param {boolean} [setToFFLGlobal] - Whether or not to call FFLSetTextureCallback on the constructed callback.
* @param {WebGLRenderingContext|WebGL2RenderingContext} [gl] - WebGL context for capability detection.
*/
constructor(module, setToFFLGlobal = false, gl = null) {
/**
* @type {Module}
* @private
*/
this._module = module;
/**
* @type {Map<number, import('three').Texture>}
* @private
*/
this._textures = new Map(); // Internal map of texture id -> THREE.Texture.
/** @package */
this._textureCallbackPtr = 0;
/**
* Controls whether or not the TextureManager
* will log creations and deletions of textures
* in order to better track memory allocations.
* @public
*/
this.logging = false;
/**
* WebGL context for capability detection
* @type {WebGLRenderingContext|WebGL2RenderingContext|null}
* @private
*/
this._gl = gl;
/**
* Whether we're running on WebGL 1
* @type {boolean}
* @private
*/
this._isWebGL1 = this._detectWebGL1();
/**
* Maximum texture size supported by the GPU
* @type {number}
* @private
*/
this._maxTextureSize = this._getMaxTextureSize();
// Create and set texture callback instance.
this._setTextureCallback();
if (setToFFLGlobal) {
// Set texture callback globally within FFL if chosen.
module._FFLSetTextureCallback(this._textureCallbackPtr);
}
}
/**
* Detects if we're running on WebGL 1
* @returns {boolean}
* @private
*/
_detectWebGL1() {
if (this._gl) {
return !(this._gl instanceof WebGL2RenderingContext);
}
// Fallback: assume WebGL1 for safety unless explicitly WebGL2
// This is more conservative and helps avoid WebGL1 compatibility issues
return true;
}
/**
* Gets the maximum texture size supported by the GPU
* @returns {number}
* @private
*/
_getMaxTextureSize() {
if (this._gl) {
return this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
}
// Conservative default for WebGL 1
return this._isWebGL1 ? 2048 : 4096;
}
/**
* Creates and allocates an {@link FFLTextureCallback} instance from callback function pointers.
* @param {Module} module - The Emscripten module.
* @param {number} createCallback - Function pointer for the create callback.
* @param {number} deleteCallback - Function pointer for the delete callback.
* @returns {number} Pointer to the {@link FFLTextureCallback}.
* Note that you MUST free this after using it (done in {@link TextureManager.disposeCallback}).
* @private
*/
static _allocateTextureCallback(module, createCallback, deleteCallback) {
const ptr = module._malloc(FFLTextureCallback.size);
const textureCallback = {
pObj: 0,
useOriginalTileMode: false,
_padding: [0, 0, 0],
pCreateFunc: createCallback,
pDeleteFunc: deleteCallback
};
const packed = FFLTextureCallback.pack(textureCallback);
module.HEAPU8.set(packed, ptr);
return ptr;
}
/**
* Creates the create/delete functions in Emscripten and allocates and sets
* the {@link FFLTextureCallback} object as {@link TextureManager._textureCallbackPtr}.
* @param {boolean} addDeleteCallback - Whether or not to bind the delete function to the texture callback.
*/
_setTextureCallback(addDeleteCallback = false) {
const mod = this._module;
// Bind the callbacks to this instance.
/** @private */
this._createCallback = mod.addFunction(this._textureCreateFunc.bind(this), 'vppp');
if (addDeleteCallback) {
/** @private */
this._deleteCallback = mod.addFunction(this._textureDeleteFunc.bind(this), 'vpp');
}
/** @private */
this._textureCallbackPtr = TextureManager._allocateTextureCallback(mod,
this._createCallback, this._deleteCallback ? this._deleteCallback : 0);
}
/**
* @param {number} format - Enum value for FFLTextureFormat.
* @returns {import('three').PixelFormat} Three.js texture format constant.
* @throws {Error} Unexpected FFLTextureFormat value
* @private
*/
_getTextureFormat(format) {
// Map FFLTextureFormat to Three.js texture formats.
// For WebGL 1 compatibility, always use Luminance formats for single/dual channel textures
const r8 = this._isWebGL1 ? THREE.LuminanceFormat : THREE.RedFormat;
const r8g8 = this._isWebGL1 ? THREE.LuminanceAlphaFormat : THREE.RGFormat;
const textureFormatToThreeFormat = {
[FFLTextureFormat.R8_UNORM]: r8,
[FFLTextureFormat.R8_G8_UNORM]: r8g8,
[FFLTextureFormat.R8_G8_B8_A8_UNORM]: THREE.RGBAFormat
};
// Determine the data format from the table.
const dataFormat = textureFormatToThreeFormat[format];
if (dataFormat === undefined) {
throw new Error(`_textureCreateFunc: Unexpected FFLTextureFormat value: ${format}`);
}
return dataFormat;
}
/**
* Checks if a dimension is a power of two
* @param {number} value
* @returns {boolean}
* @private
*/
_isPowerOfTwo(value) {
return value > 0 && (value & (value - 1)) === 0;
}
/**
* Finds the next power of two greater than or equal to the input
* @param {number} value
* @returns {number}
* @private
*/
_nextPowerOfTwo(value) {
return Math.pow(2, Math.ceil(Math.log2(value)));
}
/**
* Resizes image data to power-of-two dimensions for WebGL 1 compatibility
* @param {Uint8Array} imageData
* @param {number} width
* @param {number} height
* @param {number} channels
* @returns {{data: Uint8Array, width: number, height: number}}
* @private
*/
_resizeToPoT(imageData, width, height, channels) {
const newWidth = Math.min(this._nextPowerOfTwo(width), this._maxTextureSize);
const newHeight = Math.min(this._nextPowerOfTwo(height), this._maxTextureSize);
if (newWidth === width && newHeight === height) {
return { data: imageData, width, height };
}
if (this.logging) {
//console.debug(`Resizing texture from ${width}x${height} to ${newWidth}x${newHeight} for WebGL1 compatibility`);
}
// Improved bilinear interpolation resize
const newData = new Uint8Array(newWidth * newHeight * channels);
const xRatio = (width - 1) / newWidth;
const yRatio = (height - 1) / newHeight;
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const srcX = x * xRatio;
const srcY = y * yRatio;
const x1 = Math.floor(srcX);
const y1 = Math.floor(srcY);
const x2 = Math.min(x1 + 1, width - 1);
const y2 = Math.min(y1 + 1, height - 1);
const xWeight = srcX - x1;
const yWeight = srcY - y1;
const dstIndex = (y * newWidth + x) * channels;
for (let c = 0; c < channels; c++) {
const p1 = imageData[(y1 * width + x1) * channels + c];
const p2 = imageData[(y1 * width + x2) * channels + c];
const p3 = imageData[(y2 * width + x1) * channels + c];
const p4 = imageData[(y2 * width + x2) * channels + c];
const top = p1 * (1 - xWeight) + p2 * xWeight;
const bottom = p3 * (1 - xWeight) + p4 * xWeight;
const result = top * (1 - yWeight) + bottom * yWeight;
newData[dstIndex + c] = Math.round(result);
}
}
}
return { data: newData, width: newWidth, height: newHeight };
}
/**
* @param {number} _ - Originally pObj, unused here.
* @param {number} textureInfoPtr - Pointer to {@link FFLTextureInfo}.
* @param {number} texturePtrPtr - Pointer to the texture handle (pTexture2D).
* @private
*/
_textureCreateFunc(_, textureInfoPtr, texturePtrPtr) {
const u8 = this._module.HEAPU8.subarray(textureInfoPtr,
textureInfoPtr + FFLTextureInfo.size);
const textureInfo = FFLTextureInfo.unpack(u8);
if (this.logging) {
//console.debug(`_textureCreateFunc: width=${textureInfo.width}, height=${textureInfo.height}, format=${textureInfo.format}, imageSize=${textureInfo.imageSize}, mipCount=${textureInfo.mipCount}`);
}
/** Resolve THREE.PixelFormat. */
const format = this._getTextureFormat(textureInfo.format);
// Determine number of channels based on format
const channels = this._getChannelCount(textureInfo.format);
// Copy image data from HEAPU8 via slice. This is base level/mip level 0.
let imageData = this._module.HEAPU8.slice(textureInfo.imagePtr,
textureInfo.imagePtr + textureInfo.imageSize);
let { width, height } = textureInfo;
const originalWidth = width;
const originalHeight = height;
// Check if original dimensions are power of two
const isOriginalPoT = this._isPowerOfTwo(width) && this._isPowerOfTwo(height);
// Only resize if we're in WebGL 1 AND the texture is NPOT AND it's not too large
// Be more conservative about when to resize
const shouldResize = this._isWebGL1 && !isOriginalPoT && (width <= 256 && height <= 256);
if (shouldResize) {
const resized = this._resizeToPoT(imageData, width, height, channels);
imageData = resized.data;
width = resized.width;
height = resized.height;
if (this.logging) {
//console.debug(`Resized texture from ${originalWidth}x${originalHeight} to ${width}x${height}`);
}
}
// Final power-of-two check
const finalIsPoT = this._isPowerOfTwo(width) && this._isPowerOfTwo(height);
// Mipmaps: only if we have mipmap data and appropriate conditions
const canUseMipmaps = textureInfo.mipCount > 1 && Number(THREE.REVISION) >= 138 && !shouldResize;
const useMipmaps = canUseMipmaps && (finalIsPoT || !this._isWebGL1);
// Create new THREE.Texture with the specified format.
const texture = new THREE.DataTexture(useMipmaps ? null : imageData,
width, height, format, THREE.UnsignedByteType);
// Configure texture settings BEFORE setting needsUpdate
this._configureTextureForWebGL1(texture, width, height, useMipmaps, finalIsPoT);
if (useMipmaps) {
// Add base texture.
texture.mipmaps = [{
data: imageData,
width: width,
height: height
}];
// Enable filtering option for mipmap and add levels.
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.generateMipmaps = false;
this._addMipmaps(texture, textureInfo, channels);
}
texture.needsUpdate = true;
this.set(texture.id, texture);
this._module.HEAPU32[texturePtrPtr / 4] = texture.id;
}
/**
* Gets the number of channels for a given texture format
* @param {number} format - FFLTextureFormat enum value
* @returns {number}
* @private
*/
_getChannelCount(format) {
const channelMap = {
[FFLTextureFormat.R8_UNORM]: 1,
[FFLTextureFormat.R8_G8_UNORM]: 2,
[FFLTextureFormat.R8_G8_B8_A8_UNORM]: 4
};
return channelMap[format] || 4;
}
/**
* Configures texture settings for WebGL 1 compatibility
* @param {import('three').Texture} texture
* @param {number} width
* @param {number} height
* @param {boolean} useMipmaps
* @param {boolean} isPowerOfTwo
* @private
*/
_configureTextureForWebGL1(texture, width, height, useMipmaps, isPowerOfTwo = null) {
// Calculate if power of two if not provided
if (isPowerOfTwo === null) {
isPowerOfTwo = this._isPowerOfTwo(width) && this._isPowerOfTwo(height);
}
// Start with safe defaults for NPOT textures
if (!isPowerOfTwo || this._isWebGL1) {
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.generateMipmaps = false;
} else {
// Power-of-two textures can use repeat wrapping
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.generateMipmaps = false; // Still disable auto-generation
}
// Set filtering
texture.magFilter = THREE.LinearFilter;
if (useMipmaps && isPowerOfTwo) {
// Only use mipmap filtering if we actually have mipmaps and texture is PoT
texture.minFilter = THREE.LinearMipmapLinearFilter;
} else {
// Safe linear filtering for all other cases
texture.minFilter = THREE.LinearFilter;
}
if (this.logging) {
//console.debug(`Texture config: ${width}x${height}, PoT=${isPowerOfTwo}, mipmaps=${useMipmaps}, WebGL1=${this._isWebGL1}`);
}
}
/**
* Ensures NPOT textures are WebGL1-safe.
* @param {import('three').Texture} texture
* @private
* @deprecated Use _configureTextureForWebGL1 instead
*/
_fixNPOT(texture) {
const width = texture.image?.width;
const height = texture.image?.height;
const isPowerOfTwo = (x) => (x & (x - 1)) === 0;
if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) {
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
if (this.logging) {
//console.debug(`NPOT fix applied: ${width}x${height}`);
}
}
}
/**
* @param {import('three').Texture} texture - Texture to upload mipmaps into.
* @param {FFLTextureInfo} textureInfo - FFLTextureInfo object representing this texture.
* @param {number} channels - Number of channels in the texture
* @throws {Error} Throws if mipPtr is null.
* @private
*/
_addMipmaps(texture, textureInfo, channels) {
// Make sure mipPtr is not null.
if (textureInfo.mipPtr === 0) {
throw new Error('_addMipmaps: mipPtr is null, so the caller incorrectly assumed this texture has mipmaps');
}
// Iterate through mip levels starting from 1 (base level is mip level 0).
for (let mipLevel = 1; mipLevel < textureInfo.mipCount; mipLevel++) {
// Calculate the offset for the current mip level.
const mipOffset = textureInfo.mipLevelOffset[mipLevel - 1];
// Calculate dimensions of the current mip level.
let mipWidth = Math.max(1, textureInfo.width >> mipLevel);
let mipHeight = Math.max(1, textureInfo.height >> mipLevel);
// Get the offset of the next mipmap and calculate end offset.
const nextMipOffset = textureInfo.mipLevelOffset[mipLevel] || textureInfo.mipSize;
const end = textureInfo.mipPtr + nextMipOffset;
// Copy the data from the heap.
const start = textureInfo.mipPtr + mipOffset;
let mipData = this._module.HEAPU8.slice(start, end);
// For WebGL 1, ensure mipmap dimensions are power-of-two if needed
if (this._isWebGL1 && (!this._isPowerOfTwo(mipWidth) || !this._isPowerOfTwo(mipHeight))) {
const resized = this._resizeToPoT(mipData, mipWidth, mipHeight, channels);
mipData = resized.data;
mipWidth = resized.width;
mipHeight = resized.height;
}
if (this.logging) {
//console.debug(` - Mip ${mipLevel}: ${mipWidth}x${mipHeight}, offset=${mipOffset}, range=${start}-${end}`);
}
// Push this mip level data into the texture's mipmaps array.
// @ts-ignore - data = "CompressedTextureMipmap & CubeTexture & HTMLCanvasElement"
texture.mipmaps.push({
data: mipData, // Should still accept Uint8Array.
width: mipWidth,
height: mipHeight
});
}
}
/**
* @param {number} _ - Originally pObj, unused here.
* @param {number} texturePtrPtr - Pointer to the texture handle (pTexture2D).
* @private
*/
_textureDeleteFunc(_, texturePtrPtr) {
const texId = this._module.HEAPU32[texturePtrPtr / 4];
// this.delete(texId);
// NOTE: This is effectively no longer used as when
// we delete a CharModel instance it deletes
// cap/noseline/glass textures before we are
// finished with the model itself. It is now only logging
const tex = this._textures.get(texId);
if (tex && this.logging) {
//console.debug('Delete texture ', tex.id);
}
}
/**
* @param {number} id - ID assigned to the texture.
* @returns {import('three').Texture|null|undefined} Returns the texture if it is found.
* @public
*/
get(id) {
const texture = this._textures.get(id);
if (!texture && this.logging) {
console.error('Unknown texture', id);
}
return texture;
}
/**
* @param {number} id - ID assigned to the texture.
* @param {import('three').Texture} texture - Texture to add.
* @public
*/
set(id, texture) {
// Set texture with an override for dispose.
const disposeReal = texture.dispose.bind(texture);
texture.dispose = () => {
// Remove this texture from the map after disposing.
disposeReal();
this.delete(id); // this = TextureManager
};
this._textures.set(id, texture);
// Log is spaced to match delete/deleting/dispose messages.
if (this.logging) {
//console.debug('Adding texture ', texture.id);
}
}
/**
* @param {number} id - ID assigned to the texture.
* @public
*/
delete(id) {
// Get texture from array instead of with get()
// because it's okay if it was already deleted.
const texture = this._textures.get(id);
if (texture) {
// This is assuming the texture has already been disposed.
/** @type {Object<string, *>} */ (texture).source = null;
/** @type {Object<string, *>} */ (texture).mipmaps = null;
if (this.logging) {
//console.debug('Deleted texture ', id);
}
this._textures.delete(id);
}
}
/**
* Disposes/frees the {@link FFLTextureCallback} along with
* removing the created Emscripten functions.
* @public
*/
disposeCallback() {
// if (!this._module) {
// return;
// }
if (this._textureCallbackPtr) {
this._module._free(this._textureCallbackPtr);
this._textureCallbackPtr = 0;
}
if (this._deleteCallback) {
this._module.removeFunction(this._deleteCallback);
this._deleteCallback = 0;
}
// should always exist?:
if (this._createCallback) {
this._module.removeFunction(this._createCallback);
this._createCallback = 0;
}
// this._module = null;
}
/**
* Disposes of all textures and frees the {@link FFLTextureCallback}.
* @public
*/
dispose() {
// Dispose of all stored textures.
this._textures.forEach((tex) => {
tex.dispose();
});
// Clear texture map.
this._textures.clear();
// Free texture callback.
this.disposeCallback();
}
}
// // ---------------------------------------------------------------------
// // Classes for FFL Exceptions
// // ---------------------------------------------------------------------
// TODO PATH: src/Exceptions.js
/**
* Base exception type for all exceptions based on FFLResult.
* https://github.com/ariankordi/FFLSharp/blob/master/FFLSharp.FFLManager/FFLExceptions.cs
* https://github.com/aboood40091/ffl/blob/master/include/nn/ffl/FFLResult.h
*/
class FFLResultException extends Error {
/**
* @param {number|FFLResult} result - The returned {@link FFLResult}.
* @param {string} [funcName] - The name of the function that was called.
* @param {string} [message] - An optional message for the exception.
*/
constructor(result, funcName, message) {
if (!message) {
if (funcName) {
message = `${funcName} failed with FFLResult: ${result}`;
} else {
message = `From FFLResult: ${result}`;
}
}
super(message);
/** The stored {@link FFLResult} code. */
this.resul