Skip to content

Commit

Permalink
lazy: export lazy sprites in json
Browse files Browse the repository at this point in the history
  • Loading branch information
GarboMuffin committed Jan 2, 2025
1 parent 0246655 commit 2822b69
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 6 deletions.
39 changes: 37 additions & 2 deletions src/serialization/sb3.js
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,36 @@ const serializeTarget = function (target, extensions) {
obj.rotationStyle = target.rotationStyle;
}

// Only output anything for lazy sprites so that we match vanilla for non-lazy sprites.
if (target.lazy) {
obj.lazy = true;
}

// Add found extensions to the extensions object
targetExtensions.forEach(extensionId => {
extensions.add(extensionId);
});
return obj;
};

/**
* @param {LazySprite} lazySprite
* @param {Set} extensions
* @returns {object}
*/
const serializeLazySprite = function (lazySprite, extensions) {
if (lazySprite.state === LazySprite.State.LOADED) {
lazySprite.save();
}

const [_blocks, targetExtensions] = serializeBlocks(lazySprite.object.blocks);
targetExtensions.forEach(extensionId => {
extensions.add(extensionId);
});

return lazySprite.object;
};

/**
* @param {Record<string, unknown>} extensionStorage extensionStorage object
* @param {Set<string>} extensions extension IDs
Expand Down Expand Up @@ -720,7 +743,8 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {})

const originalTargetsToSerialize = targetId ?
[runtime.getTargetById(targetId)] :
runtime.targets.filter(target => target.isOriginal);
runtime.targets.filter(target => target.isOriginal && !target.sprite.isLazy);
const lazySpritesToSerialize = targetId ? [] : runtime.lazySprites;

const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize);

Expand All @@ -745,10 +769,17 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {})
return serialized;
});

const serializedLazySprites = lazySpritesToSerialize.map(s => serializeLazySprite(s, extensions));

const fonts = runtime.fontManager.serializeJSON();

if (targetId) {
const target = serializedTargets[0];

// Doesn't make sense for an export of a single sprite to be lazy when it gets
// imported again.
delete target.lazy;

if (extensions.size) {
// Vanilla Scratch doesn't include extensions in sprites, so don't add this if it's not needed
target.extensions = Array.from(extensions);
Expand All @@ -768,7 +799,10 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {})
obj.extensionStorage = globalExtensionStorage;
}

obj.targets = serializedTargets;
obj.targets = [
...serializedTargets,
...serializedLazySprites
];

obj.monitors = serializeMonitors(runtime.getMonitorState(), runtime, extensions);

Expand Down Expand Up @@ -1645,6 +1679,7 @@ module.exports = {
deserialize: deserialize,
deserializeBlocks: deserializeBlocks,
serializeBlocks: serializeBlocks,
serializeTarget: serializeTarget,
deserializeStandaloneBlocks: deserializeStandaloneBlocks,
serializeStandaloneBlocks: serializeStandaloneBlocks,
getExtensionIdForOpcode: getExtensionIdForOpcode,
Expand Down
4 changes: 2 additions & 2 deletions src/sprites/rendered-target.js
Original file line number Diff line number Diff line change
Expand Up @@ -1096,8 +1096,8 @@ class RenderedTarget extends Target {
tempo: this.tempo,
volume: this.volume,
videoTransparency: this.videoTransparency,
videoState: this.videoState

videoState: this.videoState,
lazy: this.sprite.isLazy
};
}

Expand Down
8 changes: 8 additions & 0 deletions src/sprites/sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ class Sprite {
}
}

/**
* True if this sprite uses lazy loading.
* @type {boolean}
*/
get isLazy () {
return false;
}

/**
* Add an array of costumes, taking care to avoid duplicate names.
* @param {!Array<object>} costumes Array of objects representing costumes.
Expand Down
8 changes: 7 additions & 1 deletion src/sprites/tw-lazy-sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ class LazySprite extends Sprite {
this._cancelLoadCallback = () => {};
}

get isLazy () {
return true;
}

/**
* Creates an instance of this sprite.
* State must be unloaded.
Expand All @@ -132,6 +136,7 @@ class LazySprite extends Sprite {
const load = async () => {
this.state = State.LOADING;

// Loaded lazily to avoid circular dependencies
const sb3 = require('../serialization/sb3');
const {
costumePromises,
Expand Down Expand Up @@ -195,7 +200,8 @@ class LazySprite extends Sprite {
const serializeAssets = require('../serialization/serialize-assets');

const target = this.clones[0];
const serializedJSON = sb3.serialize(this.runtime, target.id);
const extensions = new Set();
const serializedJSON = sb3.serializeTarget(target.toJSON(), extensions);
const assets = [
...serializeAssets.serializeCostumes(this.runtime, target.id),
...serializeAssets.serializeSounds(this.runtime, target.id)
Expand Down
61 changes: 60 additions & 1 deletion test/integration/tw_lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const {test} = require('tap');
const path = require('path');
const fs = require('fs');
const nodeCrypto = require('crypto');
const JSZip = require('@turbowarp/jszip');
const VM = require('../../src/virtual-machine');
const FakeRenderer = require('../fixtures/fake-renderer');
const makeTestStorage = require('../fixtures/make-test-storage');
Expand Down Expand Up @@ -36,7 +37,7 @@ test('lazy loaded sprite inside a zip', t => {
t.equal(lazySprite.object.name, 'Sprite1');
t.not(lazySprite.zip, null);

t.not(loadedMd5Sums.has('927d672925e7b99f7813735c484c6922'));
t.notOk(loadedMd5Sums.has('927d672925e7b99f7813735c484c6922'));

lazySprite.load().then(target => {
// Ensure sprite pointer matches
Expand All @@ -58,6 +59,16 @@ test('lazy loaded sprite inside a zip', t => {
});
});

test('isLazy === true', t => {
const vm = new VM();
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-lazy-simple.sb3'));
vm.loadProject(fixture).then(() => {
const lazySprite = vm.runtime.lazySprites[0];
t.equal(lazySprite.isLazy, true);
t.end();
});
});

test('unload before load finishes', t => {
const vm = new VM();
const renderer = new FakeRenderer();
Expand Down Expand Up @@ -174,3 +185,51 @@ test('sb2 has no lazy sprites', t => {
t.end();
});
});

for (const load of [true, false]) {
test(`export lazy sprites ${load ? 'after' : 'before'} loading`, t => {
const vm = new VM();
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-lazy-simple.sb3'));

vm.loadProject(fixture).then(async () => {
if (load) {
await vm.runtime.loadLazySprites(['Sprite1']);
}

const buffer = await vm.saveProjectSb3('arraybuffer');
const zip = await JSZip.loadAsync(buffer);
const json = JSON.parse(await zip.file('project.json').async('text'));

// Surface-level checks
t.equal(json.targets.length, 2);
t.notOk(Object.prototype.hasOwnProperty.call(json.targets[0], 'lazy'));
t.equal(json.targets[1].name, 'Sprite1');
t.equal(json.targets[1].lazy, true);

// Expect exact equality of target JSON
const fixtureZip = await JSZip.loadAsync(fixture);
const fixtureJSON = JSON.parse(await fixtureZip.file('project.json').async('text'));

delete json.targets[1].targetPaneOrder;
delete fixtureJSON.targets[1].targetPaneOrder;
delete json.targets[1].layerOrder;
delete fixtureJSON.targets[1].layerOrder;

t.same(json.targets[1], fixtureJSON.targets[1]);

t.end();
});
});
}

test('lazy sprite is not lazy when exported individually', t => {
const vm = new VM();
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-lazy-simple.sb3'));
vm.loadProject(fixture).then(() => {
vm.runtime.loadLazySprites(['Sprite1']).then(([target]) => {
const json = JSON.parse(vm.toJSON(target.id));
t.notOk(Object.prototype.hasOwnProperty.call(json, 'lazy'));
t.end();
});
});
});
9 changes: 9 additions & 0 deletions test/integration/tw_sprite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const {test} = require('tap');
const Sprite = require('../../src/sprites/sprite');
const Runtime = require('../../src/engine/runtime');

test('isLazy === false', t => {
const sprite = new Sprite(null, new Runtime());
t.equal(sprite.isLazy, false);
t.end();
});

0 comments on commit 2822b69

Please sign in to comment.