fix(live2d): encode filepaths in re-created model settings (#1838)
This commit is contained in:
@@ -30,7 +30,6 @@
|
||||
"./utils/live2d-preview": "./src/utils/live2d-preview.ts",
|
||||
"./utils/live2d-zip-loader": "./src/utils/live2d-zip-loader.ts",
|
||||
"./utils/live2d-opfs-registration": "./src/utils/live2d-opfs-registration.ts",
|
||||
"./utils/live2d-uri-encode-filenames": "./src/utils/live2d-uri-encode-filenames.ts",
|
||||
"./utils/opfs-loader": "./src/utils/opfs-loader.ts"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -443,9 +443,6 @@ async function loadModel() {
|
||||
}
|
||||
|
||||
internalModelRef.value = internalModel
|
||||
initExpressionController(internalModel).catch((err) => {
|
||||
console.warn('[Model.vue] Expression controller initialisation failed:', err)
|
||||
})
|
||||
}
|
||||
|
||||
emits('modelLoaded')
|
||||
@@ -457,6 +454,9 @@ async function loadModel() {
|
||||
finally {
|
||||
modelLoading.value = false
|
||||
componentState.value = 'mounted'
|
||||
await initExpressionController(internalModelRef.value).catch((err) => {
|
||||
console.warn('[Model.vue] Expression controller initialization failed:', err)
|
||||
})
|
||||
modelLoadMutex.release()
|
||||
}
|
||||
}
|
||||
@@ -468,11 +468,11 @@ async function loadModel() {
|
||||
* This is intentionally fire-and-forget from loadModel so that a failure in
|
||||
* expression loading does not prevent the model itself from rendering.
|
||||
*/
|
||||
async function initExpressionController(internalModel: PixiLive2DInternalModel) {
|
||||
async function initExpressionController(internalModel?: PixiLive2DInternalModel) {
|
||||
// Dispose any previous state (handles model reloads)
|
||||
expressionController.dispose()
|
||||
|
||||
const settings = (internalModel as any).settings
|
||||
const settings = internalModel?.settings as any
|
||||
if (!settings)
|
||||
return
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ export * from './stores'
|
||||
export { randomSaccadeInterval } from './utils/eye-motions'
|
||||
export * from './utils/live2d-opfs-registration'
|
||||
export * from './utils/live2d-preview'
|
||||
export * from './utils/live2d-uri-encode-filenames'
|
||||
export * from './utils/live2d-validator'
|
||||
export * from './utils/live2d-zip-loader'
|
||||
export * from './utils/opfs-loader'
|
||||
|
||||
@@ -2,7 +2,6 @@ export { randomSaccadeInterval } from './eye-motions'
|
||||
|
||||
export * from './live2d-opfs-registration'
|
||||
export * from './live2d-preview'
|
||||
export * from './live2d-uri-encode-filenames'
|
||||
export * from './live2d-validator'
|
||||
export * from './live2d-zip-loader'
|
||||
export * from './opfs-loader'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FileLoader, Live2DFactory, ZipLoader } from 'pixi-live2d-display/cubism4'
|
||||
import { Live2DFactory, ZipLoader } from 'pixi-live2d-display/cubism4'
|
||||
|
||||
import { live2dEncodeFilenamesMiddleware } from './live2d-uri-encode-filenames'
|
||||
import { OPFSCache } from './opfs-loader'
|
||||
|
||||
const zipLoaderIndex = Live2DFactory.live2DModelMiddlewares.indexOf(ZipLoader.factory)
|
||||
@@ -17,12 +16,3 @@ else if (zipLoaderIndex !== -1) {
|
||||
else {
|
||||
console.warn('[OPFS] ZipLoader not found in middlewares, caching disabled')
|
||||
}
|
||||
|
||||
// A middleware to URI-encode possible filenames in settings to handle filenames with UTF-8 characters.
|
||||
if (!Live2DFactory.live2DModelMiddlewares.includes(live2dEncodeFilenamesMiddleware)) {
|
||||
// Insert before FileLoader
|
||||
const insertBefore = Live2DFactory.live2DModelMiddlewares.indexOf(FileLoader.factory)
|
||||
if (insertBefore >= 0) {
|
||||
Live2DFactory.live2DModelMiddlewares.splice(insertBefore, 0, live2dEncodeFilenamesMiddleware)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Live2DFactoryContext, Middleware, ModelSettings } from 'pixi-live2d-display/cubism4'
|
||||
|
||||
function tryEncode(obj: any, prop: string | number) {
|
||||
if (obj?.[prop] && typeof obj[prop] === 'string') {
|
||||
obj[prop] = encodeURI(obj[prop])
|
||||
}
|
||||
}
|
||||
|
||||
// A middleware to URI-encode possible filenames in settings to handle filenames with UTF-8 characters.
|
||||
export const live2dEncodeFilenamesMiddleware: Middleware<Live2DFactoryContext> = (context, next) => {
|
||||
if (typeof context.source !== 'object' || !context.source)
|
||||
return next()
|
||||
|
||||
// Be skeptical
|
||||
const settings = context.source.settings as Partial<ModelSettings> | undefined
|
||||
if (!settings)
|
||||
return next()
|
||||
|
||||
tryEncode(settings, 'moc')
|
||||
if (Array.isArray(settings.textures)) {
|
||||
for (let i = 0; i < settings.textures.length; i++) {
|
||||
tryEncode(settings.textures, i)
|
||||
}
|
||||
}
|
||||
tryEncode(settings, 'physics')
|
||||
tryEncode(settings, 'url')
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -96,6 +96,7 @@ export class OPFSCache {
|
||||
// This avoids serving a stale model when ids are reused or props are out of sync.
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[OPFS] Cache mismatch for ${key}, source url changed`)
|
||||
await root.removeEntry(dirHandle.name, { recursive: true }) // actually invalidates cache
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -126,21 +127,6 @@ export class OPFSCache {
|
||||
writePromises.push(OPFSCache.writeFile(dirHandle, relativePath, file))
|
||||
}
|
||||
|
||||
const settingsFile = files.find(f => f.name.endsWith('.model.json') || f.name.endsWith('.model3.json'))
|
||||
|
||||
if (!settingsFile) {
|
||||
// reconstruct settings files from ModelSettings
|
||||
const settings: ModelSettings = (files as any).settings
|
||||
if (settings) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('[OPFS] Reconstructing settings file...')
|
||||
const settingsJson = JSON.stringify(settings.json)
|
||||
const settingsFileName = settings.url || 'model.model3.json'
|
||||
|
||||
writePromises.push(OPFSCache.writeFile(dirHandle, settingsFileName, settingsJson))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(writePromises)
|
||||
if (sourceUrl) {
|
||||
await OPFSCache.writeFile(dirHandle, '__meta.json', JSON.stringify({ sourceUrl }))
|
||||
@@ -219,8 +205,66 @@ export class OPFSCache {
|
||||
return next()
|
||||
}
|
||||
|
||||
const settingsFile = files.find(f => f.name.endsWith('.model.json') || f.name.endsWith('.model3.json'))
|
||||
if (!settingsFile) {
|
||||
// reconstruct settings files from ModelSettings
|
||||
const settings: ModelSettings = (files as any).settings
|
||||
if (settings) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('[OPFS] Reconstructing settings file...')
|
||||
const settingsText = encodeModelSettings(settings.json)
|
||||
const settingsFilePath = settings.url || 'model.model3.json'
|
||||
const settingsFile = new File([settingsText], settingsFilePath)
|
||||
Object.defineProperty(settingsFile, 'webkitRelativePath', {
|
||||
value: encodeURI(settingsFilePath),
|
||||
})
|
||||
files.push(settingsFile)
|
||||
}
|
||||
delete (context.source as any).settings // force the loader to read re-created settings file
|
||||
}
|
||||
await OPFSCache.save(context.opfsKey, files, context.opfsUrl)
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
function encodeProperty(obj: any, path: string) {
|
||||
let cursor = obj
|
||||
const propPath = path.split('.')
|
||||
// will lose reference when access to the last level
|
||||
while (propPath.length > 1 && cursor != null && typeof cursor === 'object' && propPath[0] in cursor) {
|
||||
cursor = cursor[propPath.shift()!]
|
||||
}
|
||||
if (cursor == null || cursor[propPath[0]] == null)
|
||||
return
|
||||
if (typeof cursor[propPath[0]] === 'string')
|
||||
cursor[propPath[0]] = encodeURI(cursor[propPath[0]])
|
||||
if (Array.isArray(cursor[propPath[0]]) && typeof cursor[propPath[0]][0] === 'string') {
|
||||
cursor[propPath[0]] = cursor[propPath[0]].map((s: string) => encodeURI(s))
|
||||
}
|
||||
}
|
||||
// TODO: find all file paths and encode them by recursively visiting the settings
|
||||
function encodeModelSettings(input: any): string {
|
||||
const settings = JSON.parse(JSON.stringify(input))
|
||||
const propertyToEncode = [
|
||||
'FileReferences.DisplayInfo',
|
||||
'FileReferences.Moc',
|
||||
'FileReferences.Textures',
|
||||
'FileReferences.Physics',
|
||||
'url',
|
||||
]
|
||||
propertyToEncode.forEach(k => encodeProperty(settings, k))
|
||||
settings?.FileReferences?.Expressions?.map((exp: { Name: string, File: string }) => {
|
||||
exp.File = encodeURI(exp.File)
|
||||
return exp
|
||||
})
|
||||
Object.keys(settings?.FileReferences?.Motions ?? {}).forEach((k) => {
|
||||
if (!Array.isArray(settings?.FileReferences?.Motions[k]))
|
||||
return // not sure whether 'Motions' is of type Record<string,[]>, assume it is for now.
|
||||
settings?.FileReferences?.Motions[k].map((exp: { File: string }) => {
|
||||
exp.File = encodeURI(exp.File)
|
||||
return exp
|
||||
})
|
||||
})
|
||||
return JSON.stringify(settings)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user