Store asset objects on costumes and sounds

Stop using storage for in-memory storage, and keep these on the vm objects. Towards https://github.com/LLK/scratch-vm/issues/1577
This commit is contained in:
Ray Schamp 2018-10-18 12:39:28 +01:00
parent 891f696570
commit b47912dce4
9 changed files with 90 additions and 90 deletions

View file

@ -11,12 +11,8 @@ const loadVector_ = function (costume, costumeAsset, runtime, rotationCenter, op
svgString = runtime.v2SvgAdapter.toString(); svgString = runtime.v2SvgAdapter.toString();
// Put back into storage // Put back into storage
const storage = runtime.storage; const storage = runtime.storage;
costumeAsset.encodeTextData(svgString, storage.DataFormat.SVG); costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true);
costume.assetId = storage.builtinHelper.cache( costume.assetId = costume.asset.assetId;
storage.AssetType.ImageVector,
storage.DataFormat.SVG,
costumeAsset.data
);
costume.md5 = `${costume.assetId}.${costume.dataFormat}`; costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
} }
// createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's
@ -63,11 +59,14 @@ const loadBitmap_ = function (costume, costumeAsset, runtime, rotationCenter) {
} else if (dataURI) { } else if (dataURI) {
// Put back into storage // Put back into storage
const storage = runtime.storage; const storage = runtime.storage;
costume.assetId = storage.builtinHelper.cache( costume.asset = storage.createAsset(
storage.AssetType.ImageBitmap, storage.AssetType.ImageBitmap,
storage.DataFormat.PNG, storage.DataFormat.PNG,
runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI) runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
null,
true // generate md5
); );
costume.assetId = costume.asset.assetId;
costume.md5 = `${costume.assetId}.${costume.dataFormat}`; costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
} }
// Regardless of if conversion succeeds, convert it to bitmap resolution 2, // Regardless of if conversion succeeds, convert it to bitmap resolution 2,
@ -163,9 +162,12 @@ const loadCostume = function (md5ext, costume, runtime, optVersion) {
const md5 = idParts[0]; const md5 = idParts[0];
const ext = idParts[1].toLowerCase(); const ext = idParts[1].toLowerCase();
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap; const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
return runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
costume.dataFormat = ext; costume.dataFormat = ext;
return (
(costume.asset && Promise.resolve(costume.asset)) ||
runtime.storage.load(assetType, md5, ext)
).then(costumeAsset => {
costume.asset = costumeAsset;
return loadCostumeFromAsset(costume, costumeAsset, runtime, optVersion); return loadCostumeFromAsset(costume, costumeAsset, runtime, optVersion);
}) })
.catch(e => { .catch(e => {

View file

@ -55,9 +55,12 @@ const loadSound = function (sound, runtime, sprite) {
const idParts = StringUtil.splitFirst(sound.md5, '.'); const idParts = StringUtil.splitFirst(sound.md5, '.');
const md5 = idParts[0]; const md5 = idParts[0];
const ext = idParts[1].toLowerCase(); const ext = idParts[1].toLowerCase();
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
.then(soundAsset => {
sound.dataFormat = ext; sound.dataFormat = ext;
return (
(sound.asset && Promise.resolve(sound.asset)) ||
runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
).then(soundAsset => {
sound.asset = soundAsset;
return loadSoundFromAsset(sound, soundAsset, runtime, sprite); return loadSoundFromAsset(sound, soundAsset, runtime, sprite);
}); });
}; };

View file

@ -22,37 +22,29 @@ const deserializeSound = function (sound, runtime, zip, assetFileName) {
return Promise.resolve(null); return Promise.resolve(null);
} }
const assetId = sound.assetId;
// TODO Is there a faster way to check that this asset
// has already been initialized?
if (storage.get(assetId)) {
// This sound has already been cached.
return Promise.resolve(null);
}
if (!zip) { // Zip will not be provided if loading project json from server if (!zip) { // Zip will not be provided if loading project json from server
return Promise.resolve(null); return Promise.resolve(null);
} }
const soundFile = zip.file(fileName); const soundFile = zip.file(fileName);
if (!soundFile) { if (!soundFile) {
log.error(`Could not find sound file associated with the ${sound.name} sound.`); log.error(`Could not find sound file associated with the ${sound.name} sound.`);
return Promise.resolve(null); return Promise.resolve(null);
} }
const dataFormat = sound.dataFormat.toLowerCase() === 'mp3' ?
storage.DataFormat.MP3 : storage.DataFormat.WAV;
if (!JSZip.support.uint8array) { if (!JSZip.support.uint8array) {
log.error('JSZip uint8array is not supported in this browser.'); log.error('JSZip uint8array is not supported in this browser.');
return Promise.resolve(null); return Promise.resolve(null);
} }
return soundFile.async('uint8array').then(data => { const dataFormat = sound.dataFormat.toLowerCase() === 'mp3' ?
storage.builtinHelper.cache( storage.DataFormat.MP3 : storage.DataFormat.WAV;
return soundFile.async('uint8array').then(data => storage.createAsset(
storage.AssetType.Sound, storage.AssetType.Sound,
dataFormat, dataFormat,
data, data,
assetId sound.assetId
); ));
});
}; };
/** /**
@ -79,14 +71,6 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) {
return Promise.resolve(null); return Promise.resolve(null);
} }
// TODO Is there a faster way to check that this asset
// has already been initialized?
if (storage.get(assetId)) {
// This costume has already been cached.
return Promise.resolve(null);
}
if (!zip) { // Zip will not be provided if loading project json from server if (!zip) { // Zip will not be provided if loading project json from server
return Promise.resolve(null); return Promise.resolve(null);
} }
@ -110,15 +94,13 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) {
return Promise.resolve(null); return Promise.resolve(null);
} }
return costumeFile.async('uint8array').then(data => { return costumeFile.async('uint8array').then(data => storage.createAsset(
storage.builtinHelper.cache(
assetType, assetType,
// TODO eventually we want to map non-png's to their actual file types? // TODO eventually we want to map non-png's to their actual file types?
costumeFormat, costumeFormat,
data, data,
assetId assetId
); ));
});
}; };
module.exports = { module.exports = {

View file

@ -452,7 +452,11 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
// the file name of the costume should be the baseLayerID followed by the file ext // the file name of the costume should be the baseLayerID followed by the file ext
const assetFileName = `${costumeSource.baseLayerID}.${ext}`; const assetFileName = `${costumeSource.baseLayerID}.${ext}`;
costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName) costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName)
.then(() => loadCostume(costume.md5, costume, runtime, 2 /* optVersion */))); .then(asset => {
costume.asset = asset;
return loadCostume(costume.md5, costume, runtime, 2 /* optVersion */);
})
);
} }
} }
// Sounds from JSON // Sounds from JSON
@ -486,7 +490,10 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
// followed by the file ext // followed by the file ext
const assetFileName = `${soundSource.soundID}.${ext}`; const assetFileName = `${soundSource.soundID}.${ext}`;
soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName) soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName)
.then(() => loadSound(sound, runtime, sprite))); .then(asset => {
sound.asset = asset;
return loadSound(sound, runtime, sprite);
}));
} }
} }

View file

@ -844,7 +844,10 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
// any translation that needs to happen will happen in the process // any translation that needs to happen will happen in the process
// of building up the costume object into an sb3 format // of building up the costume object into an sb3 format
return deserializeCostume(costume, runtime, zip) return deserializeCostume(costume, runtime, zip)
.then(() => loadCostume(costumeMd5Ext, costume, runtime)); .then(asset => {
costume.asset = asset;
return loadCostume(costumeMd5Ext, costume, runtime);
});
// Only attempt to load the costume after the deserialization // Only attempt to load the costume after the deserialization
// process has been completed // process has been completed
}); });
@ -869,7 +872,10 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
// any translation that needs to happen will happen in the process // any translation that needs to happen will happen in the process
// of building up the costume object into an sb3 format // of building up the costume object into an sb3 format
return deserializeSound(sound, runtime, zip) return deserializeSound(sound, runtime, zip)
.then(() => loadSound(sound, runtime, sprite)); .then(asset => {
sound.asset = asset;
return loadSound(sound, runtime, sprite);
});
// Only attempt to load the sound after the deserialization // Only attempt to load the sound after the deserialization
// process has been completed. // process has been completed.
}); });

View file

@ -16,12 +16,10 @@ const serializeAssets = function (runtime, assetType, optTargetId) {
const currAssets = currTarget.sprite[assetType]; const currAssets = currTarget.sprite[assetType];
for (let j = 0; j < currAssets.length; j++) { for (let j = 0; j < currAssets.length; j++) {
const currAsset = currAssets[j]; const currAsset = currAssets[j];
const assetId = currAsset.assetId; const asset = currAsset.asset;
const storage = runtime.storage;
const storedAsset = storage.get(assetId);
assetDescs.push({ assetDescs.push({
fileName: `${assetId}.${storedAsset.dataFormat}`, fileName: `${asset.assetId}.${asset.dataFormat}`,
fileContent: storedAsset.data}); fileContent: asset.data});
} }
} }
return assetDescs; return assetDescs;

View file

@ -146,14 +146,14 @@ class Sprite {
newSprite.costumes = this.costumes_.map(costume => { newSprite.costumes = this.costumes_.map(costume => {
const newCostume = Object.assign({}, costume); const newCostume = Object.assign({}, costume);
const costumeAsset = this.runtime.storage.get(costume.assetId); const costumeAsset = costume.asset;
assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime)); assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime));
return newCostume; return newCostume;
}); });
newSprite.sounds = this.sounds.map(sound => { newSprite.sounds = this.sounds.map(sound => {
const newSound = Object.assign({}, sound); const newSound = Object.assign({}, sound);
const soundAsset = this.runtime.storage.get(sound.assetId); const soundAsset = sound.asset;
assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, newSprite)); assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, newSprite));
return newSound; return newSound;
}); });

View file

@ -690,11 +690,14 @@ class VirtualMachine extends EventEmitter {
// is updated as below. // is updated as below.
sound.format = ''; sound.format = '';
const storage = this.runtime.storage; const storage = this.runtime.storage;
sound.assetId = storage.builtinHelper.cache( sound.asset = storage.createAsset(
storage.AssetType.Sound, storage.AssetType.Sound,
storage.DataFormat.WAV, storage.DataFormat.WAV,
soundEncoding soundEncoding,
null,
true // generate md5
); );
sound.assetId = sound.asset.assetId;
sound.dataFormat = storage.DataFormat.WAV; sound.dataFormat = storage.DataFormat.WAV;
sound.md5 = `${sound.assetId}.${sound.dataFormat}`; sound.md5 = `${sound.assetId}.${sound.dataFormat}`;
} }
@ -731,16 +734,16 @@ class VirtualMachine extends EventEmitter {
* a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded. * a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded.
*/ */
getCostume (costumeIndex) { getCostume (costumeIndex) {
const id = this.editingTarget.getCostumes()[costumeIndex].assetId; const asset = this.editingTarget.getCostumes()[costumeIndex].asset;
if (!id || !this.runtime || !this.runtime.storage) return null; if (!asset || !this.runtime || !this.runtime.storage) return null;
const format = this.runtime.storage.get(id).dataFormat; const format = asset.dataFormat;
if (format === this.runtime.storage.DataFormat.SVG) { if (format === this.runtime.storage.DataFormat.SVG) {
return this.runtime.storage.get(id).decodeText(); return asset.decodeText();
} else if (format === this.runtime.storage.DataFormat.PNG || } else if (format === this.runtime.storage.DataFormat.PNG ||
format === this.runtime.storage.DataFormat.JPG) { format === this.runtime.storage.DataFormat.JPG) {
return this.runtime.storage.get(id).encodeDataURI(); return asset.encodeDataURI();
} }
log.error(`Unhandled format: ${this.runtime.storage.get(id).dataFormat}`); log.error(`Unhandled format: ${asset.dataFormat}`);
return null; return null;
} }
@ -781,14 +784,17 @@ class VirtualMachine extends EventEmitter {
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener('loadend', () => { reader.addEventListener('loadend', () => {
const storage = this.runtime.storage; const storage = this.runtime.storage;
costume.assetId = storage.builtinHelper.cache(
storage.AssetType.ImageBitmap,
storage.DataFormat.PNG,
Buffer.from(reader.result)
);
costume.dataFormat = storage.DataFormat.PNG; costume.dataFormat = storage.DataFormat.PNG;
costume.bitmapResolution = bitmapResolution; costume.bitmapResolution = bitmapResolution;
costume.size = [bitmap.width, bitmap.height]; costume.size = [bitmap.width, bitmap.height];
costume.asset = storage.createAsset(
storage.AssetType.ImageBitmap,
costume.dataFormat,
Buffer.from(reader.result),
null, // id
true // generate md5
);
costume.assetId = costume.asset.assetId;
costume.md5 = `${costume.assetId}.${costume.dataFormat}`; costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
this.emitTargetsUpdate(); this.emitTargetsUpdate();
}); });
@ -812,16 +818,19 @@ class VirtualMachine extends EventEmitter {
costume.size = this.runtime.renderer.getSkinSize(costume.skinId); costume.size = this.runtime.renderer.getSkinSize(costume.skinId);
} }
const storage = this.runtime.storage; const storage = this.runtime.storage;
costume.assetId = storage.builtinHelper.cache(
storage.AssetType.ImageVector,
storage.DataFormat.SVG,
(new TextEncoder()).encode(svg)
);
// If we're in here, we've edited an svg in the vector editor, // If we're in here, we've edited an svg in the vector editor,
// so the dataFormat should be 'svg' // so the dataFormat should be 'svg'
costume.dataFormat = storage.DataFormat.SVG; costume.dataFormat = storage.DataFormat.SVG;
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
costume.bitmapResolution = 1; costume.bitmapResolution = 1;
costume.asset = storage.createAsset(
storage.AssetType.ImageVector,
costume.dataFormat,
(new TextEncoder()).encode(svg),
null,
true // generate md5
);
costume.assetId = costume.asset.assetId;
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
this.emitTargetsUpdate(); this.emitTargetsUpdate();
} }

View file

@ -10,7 +10,6 @@ const test = require('tap').test;
const AdmZip = require('adm-zip'); const AdmZip = require('adm-zip');
const ScratchStorage = require('scratch-storage'); const ScratchStorage = require('scratch-storage');
const VirtualMachine = require('../../src/index'); const VirtualMachine = require('../../src/index');
const StringUtil = require('../../src/util/string-util');
const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2'); const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2');
const projectZip = AdmZip(projectUri); const projectZip = AdmZip(projectUri);
@ -52,11 +51,8 @@ test('offline-custom-assets', t => {
t.equals(costumes.length, 1); t.equals(costumes.length, 1);
const customCostume = costumes[0]; const customCostume = costumes[0];
t.equals(customCostume.name, 'A_Test_Costume'); t.equals(customCostume.name, 'A_Test_Costume');
const costumeMd5Ext = customCostume.md5;
const costumeIdParts = StringUtil.splitFirst(costumeMd5Ext, '.');
const costumeMd5 = costumeIdParts[0];
const storedCostume = vm.runtime.storage.get(costumeMd5); const storedCostume = customCostume.asset;
t.type(storedCostume, 'object'); t.type(storedCostume, 'object');
t.deepEquals(storedCostume.data, costumeData); t.deepEquals(storedCostume.data, costumeData);
@ -64,10 +60,7 @@ test('offline-custom-assets', t => {
t.equals(sounds.length, 1); t.equals(sounds.length, 1);
const customSound = sounds[0]; const customSound = sounds[0];
t.equals(customSound.name, 'A_Test_Recording'); t.equals(customSound.name, 'A_Test_Recording');
const soundMd5Ext = customSound.md5; const storedSound = customSound.asset;
const soundIdParts = StringUtil.splitFirst(soundMd5Ext, '.');
const soundMd5 = soundIdParts[0];
const storedSound = vm.runtime.storage.get(soundMd5);
t.type(storedSound, 'object'); t.type(storedSound, 'object');
t.deepEquals(storedSound.data, soundData); t.deepEquals(storedSound.data, soundData);