Skip to content

Commit 420aa4f

Browse files
committed
lazy: export assets from unloaded lazy sprites
1 parent 2822b69 commit 420aa4f

File tree

3 files changed

+74
-18
lines changed

3 files changed

+74
-18
lines changed

src/sprites/tw-lazy-sprite.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,41 @@ class LazySprite extends Sprite {
237237
this.runtime.disposeTarget(target);
238238
}
239239
}
240+
241+
/**
242+
* Fetch all assets used in this sprite for serialization.
243+
* @returns {Promise<Array<{fileName: string; fileContents: Uint8Array}>>}
244+
*/
245+
async serializeAssets () {
246+
// Loaded lazily to avoid circular dependencies
247+
const deserializeAssets = require('../serialization/deserialize-assets');
248+
249+
const promises = [];
250+
for (const costume of this.object.costumes) {
251+
if (!costume.asset) {
252+
promises.push(deserializeAssets.deserializeCostume(costume, this.runtime, assetCacheSingleton));
253+
}
254+
}
255+
for (const sound of this.object.sounds) {
256+
if (!sound.asset) {
257+
promises.push(deserializeAssets.deserializeSound(sound, this.runtime, assetCacheSingleton));
258+
}
259+
}
260+
await Promise.all(promises);
261+
262+
const allResources = [
263+
...this.object.costumes,
264+
...this.object.sounds
265+
];
266+
267+
return allResources
268+
.map(o => (o.broken ? o.broken.asset : o.asset))
269+
.filter(asset => asset)
270+
.map(asset => ({
271+
fileName: `${asset.assetId}.${asset.dataFormat}`,
272+
fileContent: asset.data
273+
}));
274+
}
240275
}
241276

242277
// Export enums

src/virtual-machine.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -514,9 +514,9 @@ class VirtualMachine extends EventEmitter {
514514
}
515515

516516
/**
517-
* @returns {JSZip} JSZip zip object representing the sb3.
517+
* @returns {Promise<JSZip>} JSZip zip object representing the sb3.
518518
*/
519-
_saveProjectZip () {
519+
async _saveProjectZip () {
520520
const projectJson = this.toJSON();
521521

522522
// TODO want to eventually move zip creation out of here, and perhaps
@@ -525,7 +525,7 @@ class VirtualMachine extends EventEmitter {
525525

526526
// Put everything in a zip file
527527
zip.file('project.json', projectJson);
528-
this._addFileDescsToZip(this.serializeAssets(), zip);
528+
this._addFileDescsToZip(await this.serializeAssets(), zip);
529529

530530
// Use a fixed modification date for the files in the zip instead of letting JSZip use the
531531
// current time to avoid a very small metadata leak and make zipping deterministic. The magic
@@ -543,8 +543,9 @@ class VirtualMachine extends EventEmitter {
543543
* @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'blob' for Scratch compatibility.
544544
* @returns {Promise<unknown>} Compressed sb3 file in a type determined by the type argument.
545545
*/
546-
saveProjectSb3 (type) {
547-
return this._saveProjectZip().generateAsync({
546+
async saveProjectSb3 (type) {
547+
const zip = await this._saveProjectZip();
548+
return zip.generateAsync({
548549
type: type || 'blob',
549550
mimeType: 'application/x.scratch.sb3',
550551
compression: 'DEFLATE'
@@ -553,11 +554,12 @@ class VirtualMachine extends EventEmitter {
553554

554555
/**
555556
* @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'arraybuffer'.
556-
* @returns {StreamHelper} JSZip StreamHelper object generating the compressed sb3.
557+
* @returns {Promise<StreamHelper>} JSZip StreamHelper object generating the compressed sb3.
557558
* See: https://stuk.github.io/jszip/documentation/api_streamhelper.html
558559
*/
559-
saveProjectSb3Stream (type) {
560-
return this._saveProjectZip().generateInternalStream({
560+
async saveProjectSb3Stream (type) {
561+
const zip = await this._saveProjectZip();
562+
return zip.generateInternalStream({
561563
type: type || 'arraybuffer',
562564
mimeType: 'application/x.scratch.sb3',
563565
compression: 'DEFLATE'
@@ -601,19 +603,34 @@ class VirtualMachine extends EventEmitter {
601603

602604
/**
603605
* @param {string} targetId Optional ID of target to export
604-
* @returns {Array<{fileName: string; fileContent: Uint8Array;}} list of file descs
606+
* @returns {Promise<Array<{fileName: string; fileContent: Uint8Array;}>} list of file descs
605607
*/
606-
serializeAssets (targetId) {
607-
const costumeDescs = serializeCostumes(this.runtime, targetId);
608-
const soundDescs = serializeSounds(this.runtime, targetId);
608+
async serializeAssets (targetId) {
609+
// This will include non-lazy sprites and loaded lazy sprites.
610+
const loadedCostumeDescs = serializeCostumes(this.runtime, targetId);
611+
const loadedSoundDescs = serializeSounds(this.runtime, targetId);
612+
613+
// Assume every target needs all fonts.
609614
const fontDescs = this.runtime.fontManager.serializeAssets().map(asset => ({
610615
fileName: `${asset.assetId}.${asset.dataFormat}`,
611616
fileContent: asset.data
612617
}));
618+
619+
// Fetch assets used by lazy sprites.
620+
const unloadedSprites = this.runtime.lazySprites.filter(i => i.clones.length === 0);
621+
const unloadedSpriteDescs = await Promise.all(unloadedSprites.map(s => s.serializeAssets()));
622+
const flattenedUnloadedSpriteDescs = [];
623+
for (const descs of unloadedSpriteDescs) {
624+
for (const desc of descs) {
625+
flattenedUnloadedSpriteDescs.push(desc);
626+
}
627+
}
628+
613629
return [
614-
...costumeDescs,
615-
...soundDescs,
616-
...fontDescs
630+
...loadedCostumeDescs,
631+
...loadedSoundDescs,
632+
...fontDescs,
633+
...flattenedUnloadedSpriteDescs
617634
];
618635
}
619636

@@ -637,12 +654,12 @@ class VirtualMachine extends EventEmitter {
637654
* @return {object} A generated zip of the sprite and its assets in the format
638655
* specified by optZipType or blob by default.
639656
*/
640-
exportSprite (targetId, optZipType) {
657+
async exportSprite (targetId, optZipType) {
641658
const spriteJson = this.toJSON(targetId);
642659

643660
const zip = new JSZip();
644661
zip.file('sprite.json', spriteJson);
645-
this._addFileDescsToZip(this.serializeAssets(targetId), zip);
662+
this._addFileDescsToZip(await this.serializeAssets(targetId), zip);
646663

647664
return zip.generateAsync({
648665
type: typeof optZipType === 'string' ? optZipType : 'blob',

test/integration/tw_lazy.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ test('sb2 has no lazy sprites', t => {
189189
for (const load of [true, false]) {
190190
test(`export lazy sprites ${load ? 'after' : 'before'} loading`, t => {
191191
const vm = new VM();
192+
vm.attachStorage(makeTestStorage());
192193
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-lazy-simple.sb3'));
193194

194195
vm.loadProject(fixture).then(async () => {
@@ -216,7 +217,10 @@ for (const load of [true, false]) {
216217
delete fixtureJSON.targets[1].layerOrder;
217218

218219
t.same(json.targets[1], fixtureJSON.targets[1]);
219-
220+
221+
// Check for lazy loaded sprite's costume existing
222+
t.not(zip.file('927d672925e7b99f7813735c484c6922.svg'), null);
223+
220224
t.end();
221225
});
222226
});

0 commit comments

Comments
 (0)