Merge pull request #1392 from kchadha/sprite-layer-save-load

Sprite Layer Ordering Save/Load
This commit is contained in:
kchadha 2018-07-24 13:53:29 -04:00 committed by GitHub
commit c77b1f25e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 7 deletions

View file

@ -12,6 +12,7 @@ const Comment = require('../engine/comment');
const StageLayering = require('../engine/stage-layering'); const StageLayering = require('../engine/stage-layering');
const log = require('../util/log'); const log = require('../util/log');
const uid = require('../util/uid'); const uid = require('../util/uid');
const MathUtil = require('../util/math-util');
const {loadCostume} = require('../import/load-costume.js'); const {loadCostume} = require('../import/load-costume.js');
const {loadSound} = require('../import/load-sound.js'); const {loadSound} = require('../import/load-sound.js');
@ -437,6 +438,7 @@ const serializeTarget = function (target, extensions) {
obj.costumes = target.costumes.map(serializeCostume); obj.costumes = target.costumes.map(serializeCostume);
obj.sounds = target.sounds.map(serializeSound); obj.sounds = target.sounds.map(serializeSound);
if (target.hasOwnProperty('volume')) obj.volume = target.volume; if (target.hasOwnProperty('volume')) obj.volume = target.volume;
if (target.hasOwnProperty('layerOrder')) obj.layerOrder = target.layerOrder;
if (obj.isStage) { // Only the stage should have these properties if (obj.isStage) { // Only the stage should have these properties
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo; if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency; if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
@ -458,6 +460,11 @@ const serializeTarget = function (target, extensions) {
return obj; return obj;
}; };
const getSimplifiedLayerOrdering = function (targets) {
const layerOrders = targets.map(t => t.getLayerOrder());
return MathUtil.reducedSortOrdering(layerOrders);
};
/** /**
* Serializes the specified VM runtime. * Serializes the specified VM runtime.
* @param {!Runtime} runtime VM runtime instance to be serialized. * @param {!Runtime} runtime VM runtime instance to be serialized.
@ -469,9 +476,20 @@ const serialize = function (runtime, targetId) {
const obj = Object.create(null); const obj = Object.create(null);
// Create extension set to hold extension ids found while serializing targets // Create extension set to hold extension ids found while serializing targets
const extensions = new Set(); const extensions = new Set();
const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ?
const originalTargetsToSerialize = targetId ?
[runtime.getTargetById(targetId)] : [runtime.getTargetById(targetId)] :
runtime.targets.filter(target => target.isOriginal))); runtime.targets.filter(target => target.isOriginal);
const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize);
const flattenedOriginalTargets = JSON.parse(JSON.stringify(originalTargetsToSerialize));
if (runtime.renderer) {
flattenedOriginalTargets.forEach((t, index) => {
t.layerOrder = layerOrdering[index];
});
}
const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions)); const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
@ -930,6 +948,12 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
if (object.hasOwnProperty('isStage')) { if (object.hasOwnProperty('isStage')) {
target.isStage = object.isStage; target.isStage = object.isStage;
} }
if (object.hasOwnProperty('targetPaneOrder')) {
// Temporarily store the 'targetPaneOrder' property
// so that we can correctly order sprites in the target pane.
// This will be deleted after we are done parsing and ordering the targets list.
target.targetPaneOrder = object.targetPaneOrder;
}
Promise.all(costumePromises).then(costumes => { Promise.all(costumePromises).then(costumes => {
sprite.costumes = costumes; sprite.costumes = costumes;
}); });
@ -952,10 +976,28 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
extensionIDs: new Set(), extensionIDs: new Set(),
extensionURLs: new Map() extensionURLs: new Map()
}; };
// First keep track of the current target order in the json,
// then sort by the layer order property before parsing the targets
// so that their corresponding render drawables can be created in
// their layer order (e.g. back to front)
const targetObjects = ((isSingleSprite ? [json] : json.targets) || [])
.map((t, i) => Object.assign(t, {targetPaneOrder: i}))
.sort((a, b) => a.layerOrder - b.layerOrder);
return Promise.all( return Promise.all(
((isSingleSprite ? [json] : json.targets) || []).map(target => targetObjects.map(target =>
parseScratchObject(target, runtime, extensions, zip)) parseScratchObject(target, runtime, extensions, zip))
) )
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
.sort((a, b) => a.targetPaneOrder - b.targetPaneOrder)
.map(t => {
// Delete the temporary properties used for
// sprite pane ordering and stage layer ordering
delete t.targetPaneOrder;
delete t.layerOrder;
return t;
}))
.then(targets => ({ .then(targets => ({
targets, targets,
extensions extensions

View file

@ -879,6 +879,13 @@ class RenderedTarget extends Target {
return false; return false;
} }
getLayerOrder () {
if (this.renderer) {
return this.renderer.getDrawableOrder(this.drawableID);
}
return null;
}
/** /**
* Move to the front layer. * Move to the front layer.
*/ */

View file

@ -63,6 +63,20 @@ class MathUtil {
return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10)); return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10));
} }
} }
/**
* Given an array of unique numbers,
* returns a reduced array such that each element of the reduced array
* represents the position of that element in a sorted version of the
* original array.
* E.g. [5, 19. 13, 1] => [1, 3, 2, 0]
* @param {Array<number>} elts The elements to sort and reduce
* @return {Array<number>} The array of reduced orderings
*/
static reducedSortOrdering (elts) {
const sorted = elts.slice(0).sort((a, b) => a - b);
return elts.map(e => sorted.indexOf(e));
}
} }
module.exports = MathUtil; module.exports = MathUtil;

View file

@ -34,10 +34,6 @@ FakeRenderer.prototype.isTouchingColor = function (d, c) { // eslint-disable-lin
return true; return true;
}; };
FakeRenderer.prototype.setDrawableOrder = function (d, l, optA, optB) { // eslint-disable-line no-unused-vars
return true;
};
FakeRenderer.prototype.getBounds = function (d) { // eslint-disable-line no-unused-vars FakeRenderer.prototype.getBounds = function (d) { // eslint-disable-line no-unused-vars
return {left: this.x, right: this.x, top: this.y, bottom: this.y}; return {left: this.x, right: this.x, top: this.y, bottom: this.y};
}; };
@ -55,6 +51,10 @@ FakeRenderer.prototype.setDrawableOrder = function (d, a, optG, optA, optB) { //
return this.order; return this.order;
}; };
FakeRenderer.prototype.getDrawableOrder = function (d) { // eslint-disable-line no-unused-vars
return 'stub';
};
FakeRenderer.prototype.pick = function (x, y, a, b, c) { // eslint-disable-line no-unused-vars FakeRenderer.prototype.pick = function (x, y, a, b, c) { // eslint-disable-line no-unused-vars
return c[0]; return c[0];
}; };

View file

@ -7,6 +7,7 @@ const exampleProjectPath = path.resolve(__dirname, '../fixtures/clone-cleanup.sb
const commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2'); const commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2');
const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3'); const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3');
const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3'); const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3');
const FakeRenderer = require('../fixtures/fake-renderer');
test('serialize', t => { test('serialize', t => {
const vm = new VirtualMachine(); const vm = new VirtualMachine();
@ -142,3 +143,42 @@ test('deserialize sb3 project with comments - no duplicate id serialization', t
t.end(); t.end();
}); });
}); });
test('serialize sb3 preserves sprite layer order', t => {
const vm = new VirtualMachine();
vm.attachRenderer(new FakeRenderer());
vm.loadProject(readFileToBuffer(path.resolve(__dirname, '../fixtures/ordering.sb2')))
.then(() => {
// Target get layer order needs a renderer,
// fake the numbers we would get back from the
// renderer in order to test that they are serialized
// correctly
vm.runtime.targets[0].getLayerOrder = () => 0;
vm.runtime.targets[1].getLayerOrder = () => 20;
vm.runtime.targets[2].getLayerOrder = () => 10;
vm.runtime.targets[3].getLayerOrder = () => 30;
const result = sb3.serialize(vm.runtime);
t.type(JSON.stringify(result), 'string');
t.type(result.targets, 'object');
t.equal(Array.isArray(result.targets), true);
t.equal(result.targets.length, 4);
// First check that the sprites are ordered correctly (as they would
// appear in the target pane)
t.equal(result.targets[0].name, 'Stage');
t.equal(result.targets[1].name, 'First');
t.equal(result.targets[2].name, 'Second');
t.equal(result.targets[3].name, 'Third');
// Check that they are in the correct layer order (as they would render
// back to front on the stage)
t.equal(result.targets[0].layerOrder, 0);
t.equal(result.targets[1].layerOrder, 2);
t.equal(result.targets[2].layerOrder, 1);
t.equal(result.targets[3].layerOrder, 3);
t.end();
});
});

View file

@ -326,6 +326,23 @@ test('layers', t => { // TODO this tests fake functionality. Move layering tests
t.end(); t.end();
}); });
test('getLayerOrder returns result of renderer getDrawableOrder or null if renderer is not attached', t => {
const s = new Sprite();
const r = new Runtime();
const a = new RenderedTarget(s, r);
// getLayerOrder should return null if there is no renderer attached to the runtime
t.equal(a.getLayerOrder(), null);
const renderer = new FakeRenderer();
r.attachRenderer(renderer);
const b = new RenderedTarget(s, r);
t.equal(b.getLayerOrder(), 'stub');
t.end();
});
test('keepInFence', t => { test('keepInFence', t => {
const s = new Sprite(); const s = new Sprite();
const r = new Runtime(); const r = new Runtime();

View file

@ -42,3 +42,9 @@ test('tan', t => {
t.strictEqual(math.tan(33), 0.6494075932); t.strictEqual(math.tan(33), 0.6494075932);
t.end(); t.end();
}); });
test('reducedSortOrdering', t => {
t.deepEqual(math.reducedSortOrdering([5, 18, 6, 3]), [1, 3, 2, 0]);
t.deepEqual(math.reducedSortOrdering([5, 1, 56, 19]), [1, 0, 3, 2]);
t.end();
});