From 31fbcfa4d75c315ed60c2d2e975b4c1d572bc363 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 6 Feb 2019 10:16:22 -0500 Subject: [PATCH] Add a utility for giving blocks new IDs, use it for sharing blocks. --- src/util/new-block-ids.js | 33 +++++++++++++++++ src/virtual-machine.js | 2 + test/fixtures/simple-stack.js | 65 +++++++++++++++++++++++++++++++++ test/unit/util_new-block-ids.js | 64 ++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 src/util/new-block-ids.js create mode 100644 test/fixtures/simple-stack.js create mode 100644 test/unit/util_new-block-ids.js diff --git a/src/util/new-block-ids.js b/src/util/new-block-ids.js new file mode 100644 index 000000000..1fc5f6967 --- /dev/null +++ b/src/util/new-block-ids.js @@ -0,0 +1,33 @@ +const uid = require('./uid'); + +/** + * Mutate the given blocks to have new IDs and update all internal ID references. + * Does not return anything to make it clear that the blocks are updated in-place. + * @param {array} blocks - blocks to be mutated. + */ +module.exports = blocks => { + const oldToNew = {}; + + // First update all top-level IDs and create old-to-new mapping + for (let i = 0; i < blocks.length; i++) { + const newId = uid(); + const oldId = blocks[i].id; + blocks[i].id = oldToNew[oldId] = newId; + } + + // Then go back through and update inputs (block/shadow) + // and next/parent properties + for (let i = 0; i < blocks.length; i++) { + for (const key in blocks[i].inputs) { + const input = blocks[i].inputs[key]; + input.block = oldToNew[input.block]; + input.shadow = oldToNew[input.shadow]; + } + if (blocks[i].parent) { + blocks[i].parent = oldToNew[blocks[i].parent]; + } + if (blocks[i].next) { + blocks[i].next = oldToNew[blocks[i].next]; + } + } +}; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 38eb79e9f..a933b8c04 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -16,6 +16,7 @@ const formatMessage = require('format-message'); const validate = require('scratch-parser'); const Variable = require('./engine/variable'); +const newBlockIds = require('./util/new-block-ids'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); @@ -1178,6 +1179,7 @@ class VirtualMachine extends EventEmitter { */ shareBlocksToTarget (blocks, targetId, optFromTargetId) { const copiedBlocks = JSON.parse(JSON.stringify(blocks)); + newBlockIds(copiedBlocks); const target = this.runtime.getTargetById(targetId); if (optFromTargetId) { diff --git a/test/fixtures/simple-stack.js b/test/fixtures/simple-stack.js new file mode 100644 index 000000000..acd5f5d3a --- /dev/null +++ b/test/fixtures/simple-stack.js @@ -0,0 +1,65 @@ +module.exports = [ + { + id: '1ZGd(W8DvU?vI1RN)e0E', + opcode: 'motion_goto', + inputs: { + TO: { + name: 'TO', + block: 'Ht(rOKMe0@sB3n4(b3;=', + shadow: '-C=c-_TI_7d(l3ii2[wh' + } + }, + fields: { + + }, + next: 'l.JBk`WcXE+A@i9y1tCU', + topLevel: true, + parent: null, + shadow: false + }, + { + id: 'Ht(rOKMe0@sB3n4(b3;=', + opcode: 'looks_size', + inputs: { + + }, + fields: { + + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: false + }, + { + id: '-C=c-_TI_7d(l3ii2[wh', + opcode: 'motion_goto_menu', + inputs: { + + }, + fields: { + TO: { + name: 'TO', + value: '_random_' + } + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: true + }, + { + id: 'l.JBk`WcXE+A@i9y1tCU', + opcode: 'sound_stopallsounds', + inputs: { + + }, + fields: { + + }, + next: null, + topLevel: false, + parent: '1ZGd(W8DvU?vI1RN)e0E', + shadow: false + } +]; diff --git a/test/unit/util_new-block-ids.js b/test/unit/util_new-block-ids.js new file mode 100644 index 000000000..6b5d3038c --- /dev/null +++ b/test/unit/util_new-block-ids.js @@ -0,0 +1,64 @@ +const newBlockIds = require('../../src/util/new-block-ids'); +const simpleStack = require('../fixtures/simple-stack'); +const tap = require('tap'); +const test = tap.test; + +let originals; +let newBlocks; + +tap.beforeEach(done => { + originals = simpleStack; + // Will be mutated so make a copy first + newBlocks = JSON.parse(JSON.stringify(simpleStack)); + newBlockIds(newBlocks); + done(); +}); + + +/** + * The structure of the simple stack is: + * moveTo (looks_size) -> stopAllSounds + * The list of blocks is + * 0: moveTo (TO input block: 1, shadow: 2) + * 1: looks_size (parent: 0) + * 2: obscured shadow for moveTo input (parent: 0) + * 3: stopAllSounds (parent: 0) + * Inspect fixtures/blocks for the full object. + */ + +test('top-level block IDs have all changed', t => { + newBlocks.forEach((block, i) => { + t.notEqual(block.id, originals[i].id); + }); + t.end(); +}); + +test('input reference is maintained on parent for attached block', t => { + t.equal(newBlocks[0].inputs.TO.block, newBlocks[1].id); + t.end(); +}); + +test('input reference is maintained on parent for obscured shadow', t => { + t.equal(newBlocks[0].inputs.TO.shadow, newBlocks[2].id); + t.end(); +}); + +test('parent reference is maintained for attached input', t => { + t.equal(newBlocks[1].parent, newBlocks[0].id); + t.end(); +}); + +test('parent reference is maintained for obscured shadow', t => { + t.equal(newBlocks[2].parent, newBlocks[0].id); + t.end(); +}); + +test('parent reference is maintained for next block', t => { + t.equal(newBlocks[3].parent, newBlocks[0].id); + t.end(); +}); + +test('next reference is maintained for previous block', t => { + t.equal(newBlocks[0].next, newBlocks[3].id); + t.end(); +});