@scratch/scratch-vm 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
const adapter = require('./adapter');
|
|
2
|
+
const mutationAdapter = require('./mutation-adapter');
|
|
3
|
+
const xmlEscape = require('../util/xml-escape');
|
|
4
|
+
const MonitorRecord = require('./monitor-record');
|
|
5
|
+
const Clone = require('../util/clone');
|
|
6
|
+
const {Map} = require('immutable');
|
|
7
|
+
const BlocksExecuteCache = require('./blocks-execute-cache');
|
|
8
|
+
const BlocksRuntimeCache = require('./blocks-runtime-cache');
|
|
9
|
+
const log = require('../util/log');
|
|
10
|
+
const Variable = require('./variable');
|
|
11
|
+
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @fileoverview
|
|
15
|
+
* Store and mutate the VM block representation,
|
|
16
|
+
* and handle updates from Scratch Blocks events.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a block container.
|
|
21
|
+
* @param {Runtime} runtime The runtime this block container operates within
|
|
22
|
+
* @param {boolean} optNoGlow Optional flag to indicate that blocks in this container
|
|
23
|
+
* should not request glows. This does not affect glows when clicking on a block to execute it.
|
|
24
|
+
*/
|
|
25
|
+
class Blocks {
|
|
26
|
+
constructor (runtime, optNoGlow) {
|
|
27
|
+
this.runtime = runtime;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* All blocks in the workspace.
|
|
31
|
+
* Keys are block IDs, values are metadata about the block.
|
|
32
|
+
* @type {Object.<string, Object>}
|
|
33
|
+
*/
|
|
34
|
+
this._blocks = {};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* All top-level scripts in the workspace.
|
|
38
|
+
* A list of block IDs that represent scripts (i.e., first block in script).
|
|
39
|
+
* @type {Array.<String>}
|
|
40
|
+
*/
|
|
41
|
+
this._scripts = [];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runtime Cache
|
|
45
|
+
* @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(this, '_cache', {writable: true, enumerable: false});
|
|
49
|
+
this._cache = {
|
|
50
|
+
/**
|
|
51
|
+
* Cache block inputs by block id
|
|
52
|
+
* @type {object.<string, !Array.<object>>}
|
|
53
|
+
*/
|
|
54
|
+
inputs: {},
|
|
55
|
+
/**
|
|
56
|
+
* Cache procedure Param Names by block id
|
|
57
|
+
* @type {object.<string, ?Array.<string>>}
|
|
58
|
+
*/
|
|
59
|
+
procedureParamNames: {},
|
|
60
|
+
/**
|
|
61
|
+
* Cache procedure definitions by block id
|
|
62
|
+
* @type {object.<string, ?string>}
|
|
63
|
+
*/
|
|
64
|
+
procedureDefinitions: {},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A cache for execute to use and store on. Only available to
|
|
68
|
+
* execute.
|
|
69
|
+
* @type {object.<string, object>}
|
|
70
|
+
*/
|
|
71
|
+
_executeCached: {},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A cache of block IDs and targets to start threads on as they are
|
|
75
|
+
* actively monitored.
|
|
76
|
+
* @type {Array<{blockId: string, target: Target}>}
|
|
77
|
+
*/
|
|
78
|
+
_monitored: null,
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A cache of hat opcodes to collection of theads to execute.
|
|
82
|
+
* @type {object.<string, object>}
|
|
83
|
+
*/
|
|
84
|
+
scripts: {}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flag which indicates that blocks in this container should not glow.
|
|
89
|
+
* Blocks will still glow when clicked on, but this flag is used to control
|
|
90
|
+
* whether the blocks in this container can request a glow as part of
|
|
91
|
+
* a running stack. E.g. the flyout block container and the monitor block container
|
|
92
|
+
* should not be able to request a glow, but blocks containers belonging to
|
|
93
|
+
* sprites should.
|
|
94
|
+
* @type {boolean}
|
|
95
|
+
*/
|
|
96
|
+
this.forceNoGlow = optNoGlow || false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Blockly inputs that represent statements/branch.
|
|
101
|
+
* are prefixed with this string.
|
|
102
|
+
* @const{string}
|
|
103
|
+
*/
|
|
104
|
+
static get BRANCH_INPUT_PREFIX () {
|
|
105
|
+
return 'SUBSTACK';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Provide an object with metadata for the requested block ID.
|
|
110
|
+
* @param {!string} blockId ID of block we have stored.
|
|
111
|
+
* @return {?object} Metadata about the block, if it exists.
|
|
112
|
+
*/
|
|
113
|
+
getBlock (blockId) {
|
|
114
|
+
return this._blocks[blockId];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all known top-level blocks that start scripts.
|
|
119
|
+
* @return {Array.<string>} List of block IDs.
|
|
120
|
+
*/
|
|
121
|
+
getScripts () {
|
|
122
|
+
return this._scripts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the next block for a particular block
|
|
127
|
+
* @param {?string} id ID of block to get the next block for
|
|
128
|
+
* @return {?string} ID of next block in the sequence
|
|
129
|
+
*/
|
|
130
|
+
getNextBlock (id) {
|
|
131
|
+
const block = this._blocks[id];
|
|
132
|
+
return (typeof block === 'undefined') ? null : block.next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the branch for a particular C-shaped block.
|
|
137
|
+
* @param {?string} id ID for block to get the branch for.
|
|
138
|
+
* @param {?number} branchNum Which branch to select (e.g. for if-else).
|
|
139
|
+
* @return {?string} ID of block in the branch.
|
|
140
|
+
*/
|
|
141
|
+
getBranch (id, branchNum) {
|
|
142
|
+
const block = this._blocks[id];
|
|
143
|
+
if (typeof block === 'undefined') return null;
|
|
144
|
+
if (!branchNum) branchNum = 1;
|
|
145
|
+
|
|
146
|
+
let inputName = Blocks.BRANCH_INPUT_PREFIX;
|
|
147
|
+
if (branchNum > 1) {
|
|
148
|
+
inputName += branchNum;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Empty C-block?
|
|
152
|
+
const input = block.inputs[inputName];
|
|
153
|
+
return (typeof input === 'undefined') ? null : input.block;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the opcode for a particular block
|
|
158
|
+
* @param {?object} block The block to query
|
|
159
|
+
* @return {?string} the opcode corresponding to that block
|
|
160
|
+
*/
|
|
161
|
+
getOpcode (block) {
|
|
162
|
+
return (typeof block === 'undefined') ? null : block.opcode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get all fields and their values for a block.
|
|
167
|
+
* @param {?object} block The block to query.
|
|
168
|
+
* @return {?object} All fields and their values.
|
|
169
|
+
*/
|
|
170
|
+
getFields (block) {
|
|
171
|
+
return (typeof block === 'undefined') ? null : block.fields;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get all non-branch inputs for a block.
|
|
176
|
+
* @param {?object} block the block to query.
|
|
177
|
+
* @return {?Array.<object>} All non-branch inputs and their associated blocks.
|
|
178
|
+
*/
|
|
179
|
+
getInputs (block) {
|
|
180
|
+
if (typeof block === 'undefined') return null;
|
|
181
|
+
let inputs = this._cache.inputs[block.id];
|
|
182
|
+
if (typeof inputs !== 'undefined') {
|
|
183
|
+
return inputs;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
inputs = {};
|
|
187
|
+
for (const input in block.inputs) {
|
|
188
|
+
// Ignore blocks prefixed with branch prefix.
|
|
189
|
+
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
|
|
190
|
+
Blocks.BRANCH_INPUT_PREFIX) {
|
|
191
|
+
inputs[input] = block.inputs[input];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this._cache.inputs[block.id] = inputs;
|
|
196
|
+
return inputs;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get mutation data for a block.
|
|
201
|
+
* @param {?object} block The block to query.
|
|
202
|
+
* @return {?object} Mutation for the block.
|
|
203
|
+
*/
|
|
204
|
+
getMutation (block) {
|
|
205
|
+
return (typeof block === 'undefined') ? null : block.mutation;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the top-level script for a given block.
|
|
210
|
+
* @param {?string} id ID of block to query.
|
|
211
|
+
* @return {?string} ID of top-level script block.
|
|
212
|
+
*/
|
|
213
|
+
getTopLevelScript (id) {
|
|
214
|
+
let block = this._blocks[id];
|
|
215
|
+
if (typeof block === 'undefined') return null;
|
|
216
|
+
while (block.parent !== null) {
|
|
217
|
+
block = this._blocks[block.parent];
|
|
218
|
+
}
|
|
219
|
+
return block.id;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the procedure definition for a given name.
|
|
224
|
+
* @param {?string} name Name of procedure to query.
|
|
225
|
+
* @return {?string} ID of procedure definition.
|
|
226
|
+
*/
|
|
227
|
+
getProcedureDefinition (name) {
|
|
228
|
+
const blockID = this._cache.procedureDefinitions[name];
|
|
229
|
+
if (typeof blockID !== 'undefined') {
|
|
230
|
+
return blockID;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const id in this._blocks) {
|
|
234
|
+
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
|
|
235
|
+
const block = this._blocks[id];
|
|
236
|
+
if (block.opcode === 'procedures_definition') {
|
|
237
|
+
const internal = this._getCustomBlockInternal(block);
|
|
238
|
+
if (internal && internal.mutation.proccode === name) {
|
|
239
|
+
this._cache.procedureDefinitions[name] = id; // The outer define block id
|
|
240
|
+
return id;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this._cache.procedureDefinitions[name] = null;
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get names and ids of parameters for the given procedure.
|
|
251
|
+
* @param {?string} name Name of procedure to query.
|
|
252
|
+
* @return {?Array.<string>} List of param names for a procedure.
|
|
253
|
+
*/
|
|
254
|
+
getProcedureParamNamesAndIds (name) {
|
|
255
|
+
return this.getProcedureParamNamesIdsAndDefaults(name).slice(0, 2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get names, ids, and defaults of parameters for the given procedure.
|
|
260
|
+
* @param {?string} name Name of procedure to query.
|
|
261
|
+
* @return {?Array.<string>} List of param names for a procedure.
|
|
262
|
+
*/
|
|
263
|
+
getProcedureParamNamesIdsAndDefaults (name) {
|
|
264
|
+
const cachedNames = this._cache.procedureParamNames[name];
|
|
265
|
+
if (typeof cachedNames !== 'undefined') {
|
|
266
|
+
return cachedNames;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const id in this._blocks) {
|
|
270
|
+
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
|
|
271
|
+
const block = this._blocks[id];
|
|
272
|
+
if (block.opcode === 'procedures_prototype' &&
|
|
273
|
+
block.mutation.proccode === name) {
|
|
274
|
+
const names = JSON.parse(block.mutation.argumentnames);
|
|
275
|
+
const ids = JSON.parse(block.mutation.argumentids);
|
|
276
|
+
const defaults = JSON.parse(block.mutation.argumentdefaults);
|
|
277
|
+
|
|
278
|
+
this._cache.procedureParamNames[name] = [names, ids, defaults];
|
|
279
|
+
return this._cache.procedureParamNames[name];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._cache.procedureParamNames[name] = null;
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
duplicate () {
|
|
288
|
+
const newBlocks = new Blocks(this.runtime, this.forceNoGlow);
|
|
289
|
+
newBlocks._blocks = Clone.simple(this._blocks);
|
|
290
|
+
newBlocks._scripts = Clone.simple(this._scripts);
|
|
291
|
+
return newBlocks;
|
|
292
|
+
}
|
|
293
|
+
// ---------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create event listener for blocks, variables, and comments. Handles validation and
|
|
297
|
+
* serves as a generic adapter between the blocks, variables, and the
|
|
298
|
+
* runtime interface.
|
|
299
|
+
* @param {object} e Blockly "block" or "variable" event
|
|
300
|
+
*/
|
|
301
|
+
blocklyListen (e) {
|
|
302
|
+
// Validate event
|
|
303
|
+
if (typeof e !== 'object') return;
|
|
304
|
+
if (typeof e.blockId !== 'string' && typeof e.varId !== 'string' &&
|
|
305
|
+
typeof e.commentId !== 'string') {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const stage = this.runtime.getTargetForStage();
|
|
309
|
+
const editingTarget = this.runtime.getEditingTarget();
|
|
310
|
+
|
|
311
|
+
// UI event: clicked scripts toggle in the runtime.
|
|
312
|
+
if (e.element === 'stackclick') {
|
|
313
|
+
this.runtime.toggleScript(e.blockId, {stackClick: true});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Block create/update/destroy
|
|
318
|
+
switch (e.type) {
|
|
319
|
+
case 'create': {
|
|
320
|
+
const newBlocks = adapter(e);
|
|
321
|
+
// A create event can create many blocks. Add them all.
|
|
322
|
+
for (let i = 0; i < newBlocks.length; i++) {
|
|
323
|
+
this.createBlock(newBlocks[i]);
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'change':
|
|
328
|
+
this.changeBlock({
|
|
329
|
+
id: e.blockId,
|
|
330
|
+
element: e.element,
|
|
331
|
+
name: e.name,
|
|
332
|
+
value: e.newValue
|
|
333
|
+
});
|
|
334
|
+
break;
|
|
335
|
+
case 'move':
|
|
336
|
+
this.moveBlock({
|
|
337
|
+
id: e.blockId,
|
|
338
|
+
oldParent: e.oldParentId,
|
|
339
|
+
oldInput: e.oldInputName,
|
|
340
|
+
newParent: e.newParentId,
|
|
341
|
+
newInput: e.newInputName,
|
|
342
|
+
newCoordinate: e.newCoordinate
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
case 'dragOutside':
|
|
346
|
+
this.runtime.emitBlockDragUpdate(e.isOutside);
|
|
347
|
+
break;
|
|
348
|
+
case 'endDrag':
|
|
349
|
+
this.runtime.emitBlockDragUpdate(false /* areBlocksOverGui */);
|
|
350
|
+
|
|
351
|
+
// Drag blocks onto another sprite
|
|
352
|
+
if (e.isOutside) {
|
|
353
|
+
const newBlocks = adapter(e);
|
|
354
|
+
this.runtime.emitBlockEndDrag(newBlocks, e.blockId);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case 'delete':
|
|
358
|
+
// Don't accept delete events for missing blocks,
|
|
359
|
+
// or shadow blocks being obscured.
|
|
360
|
+
if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId) ||
|
|
361
|
+
this._blocks[e.blockId].shadow) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Inform any runtime to forget about glows on this script.
|
|
365
|
+
if (this._blocks[e.blockId].topLevel) {
|
|
366
|
+
this.runtime.quietGlow(e.blockId);
|
|
367
|
+
}
|
|
368
|
+
this.deleteBlock(e.blockId);
|
|
369
|
+
break;
|
|
370
|
+
case 'var_create':
|
|
371
|
+
// Check if the variable being created is global or local
|
|
372
|
+
// If local, create a local var on the current editing target, as long
|
|
373
|
+
// as there are no conflicts, and the current target is actually a sprite
|
|
374
|
+
// If global or if the editing target is not present or we somehow got
|
|
375
|
+
// into a state where a local var was requested for the stage,
|
|
376
|
+
// create a stage (global) var after checking for name conflicts
|
|
377
|
+
// on all the sprites.
|
|
378
|
+
if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) {
|
|
379
|
+
if (!editingTarget.lookupVariableById(e.varId)) {
|
|
380
|
+
editingTarget.createVariable(e.varId, e.varName, e.varType);
|
|
381
|
+
this.emitProjectChanged();
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
if (stage.lookupVariableById(e.varId)) {
|
|
385
|
+
// Do not re-create a variable if it already exists
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// Check for name conflicts in all of the targets
|
|
389
|
+
const allTargets = this.runtime.targets.filter(t => t.isOriginal);
|
|
390
|
+
for (const target of allTargets) {
|
|
391
|
+
if (target.lookupVariableByNameAndType(e.varName, e.varType, true)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
stage.createVariable(e.varId, e.varName, e.varType, e.isCloud);
|
|
396
|
+
this.emitProjectChanged();
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
case 'var_rename':
|
|
400
|
+
if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) {
|
|
401
|
+
// This is a local variable, rename on the current target
|
|
402
|
+
editingTarget.renameVariable(e.varId, e.newName);
|
|
403
|
+
// Update all the blocks on the current target that use
|
|
404
|
+
// this variable
|
|
405
|
+
editingTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName);
|
|
406
|
+
} else {
|
|
407
|
+
// This is a global variable
|
|
408
|
+
stage.renameVariable(e.varId, e.newName);
|
|
409
|
+
// Update all blocks on all targets that use the renamed variable
|
|
410
|
+
const targets = this.runtime.targets;
|
|
411
|
+
for (let i = 0; i < targets.length; i++) {
|
|
412
|
+
const currTarget = targets[i];
|
|
413
|
+
currTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.emitProjectChanged();
|
|
417
|
+
break;
|
|
418
|
+
case 'var_delete': {
|
|
419
|
+
const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) ?
|
|
420
|
+
editingTarget : stage;
|
|
421
|
+
target.deleteVariable(e.varId);
|
|
422
|
+
this.emitProjectChanged();
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case 'comment_create':
|
|
426
|
+
if (this.runtime.getEditingTarget()) {
|
|
427
|
+
const currTarget = this.runtime.getEditingTarget();
|
|
428
|
+
currTarget.createComment(e.commentId, e.blockId, e.text,
|
|
429
|
+
e.xy.x, e.xy.y, e.width, e.height, e.minimized);
|
|
430
|
+
|
|
431
|
+
if (currTarget.comments[e.commentId].x === null &&
|
|
432
|
+
currTarget.comments[e.commentId].y === null) {
|
|
433
|
+
// Block comments imported from 2.0 projects are imported with their
|
|
434
|
+
// x and y coordinates set to null so that scratch-blocks can
|
|
435
|
+
// auto-position them. If we are receiving a create event for these
|
|
436
|
+
// comments, then the auto positioning should have taken place.
|
|
437
|
+
// Update the x and y position of these comments to match the
|
|
438
|
+
// one from the event.
|
|
439
|
+
currTarget.comments[e.commentId].x = e.xy.x;
|
|
440
|
+
currTarget.comments[e.commentId].y = e.xy.y;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this.emitProjectChanged();
|
|
444
|
+
break;
|
|
445
|
+
case 'comment_change':
|
|
446
|
+
if (this.runtime.getEditingTarget()) {
|
|
447
|
+
const currTarget = this.runtime.getEditingTarget();
|
|
448
|
+
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
|
|
449
|
+
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const comment = currTarget.comments[e.commentId];
|
|
453
|
+
const change = e.newContents_;
|
|
454
|
+
if (Object.prototype.hasOwnProperty.call(change, 'minimized')) {
|
|
455
|
+
comment.minimized = change.minimized;
|
|
456
|
+
}
|
|
457
|
+
if (Object.prototype.hasOwnProperty.call(change, 'width') &&
|
|
458
|
+
Object.prototype.hasOwnProperty.call(change, 'height')) {
|
|
459
|
+
comment.width = change.width;
|
|
460
|
+
comment.height = change.height;
|
|
461
|
+
}
|
|
462
|
+
if (Object.prototype.hasOwnProperty.call(change, 'text')) {
|
|
463
|
+
comment.text = change.text;
|
|
464
|
+
}
|
|
465
|
+
this.emitProjectChanged();
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
case 'comment_move':
|
|
469
|
+
if (this.runtime.getEditingTarget()) {
|
|
470
|
+
const currTarget = this.runtime.getEditingTarget();
|
|
471
|
+
if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
|
|
472
|
+
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const comment = currTarget.comments[e.commentId];
|
|
476
|
+
const newCoord = e.newCoordinate_;
|
|
477
|
+
comment.x = newCoord.x;
|
|
478
|
+
comment.y = newCoord.y;
|
|
479
|
+
|
|
480
|
+
this.emitProjectChanged();
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
case 'comment_delete':
|
|
484
|
+
if (this.runtime.getEditingTarget()) {
|
|
485
|
+
const currTarget = this.runtime.getEditingTarget();
|
|
486
|
+
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
|
|
487
|
+
// If we're in this state, we have probably received
|
|
488
|
+
// a delete event from a workspace that we switched from
|
|
489
|
+
// (e.g. a delete event for a comment on sprite a's workspace
|
|
490
|
+
// when switching from sprite a to sprite b)
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
delete currTarget.comments[e.commentId];
|
|
494
|
+
if (e.blockId) {
|
|
495
|
+
const block = currTarget.blocks.getBlock(e.blockId);
|
|
496
|
+
if (!block) {
|
|
497
|
+
log.warn(`Could not find block referenced by comment with id: ${e.commentId}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
delete block.comment;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
this.emitProjectChanged();
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ---------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Reset all runtime caches.
|
|
513
|
+
*/
|
|
514
|
+
resetCache () {
|
|
515
|
+
this._cache.inputs = {};
|
|
516
|
+
this._cache.procedureParamNames = {};
|
|
517
|
+
this._cache.procedureDefinitions = {};
|
|
518
|
+
this._cache._executeCached = {};
|
|
519
|
+
this._cache._monitored = null;
|
|
520
|
+
this._cache.scripts = {};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Emit a project changed event if this is a block container
|
|
525
|
+
* that can affect the project state.
|
|
526
|
+
*/
|
|
527
|
+
emitProjectChanged () {
|
|
528
|
+
if (!this.forceNoGlow) {
|
|
529
|
+
this.runtime.emitProjectChanged();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Block management: create blocks and scripts from a `create` event
|
|
535
|
+
* @param {!object} block Blockly create event to be processed
|
|
536
|
+
*/
|
|
537
|
+
createBlock (block) {
|
|
538
|
+
// Does the block already exist?
|
|
539
|
+
// Could happen, e.g., for an unobscured shadow.
|
|
540
|
+
if (Object.prototype.hasOwnProperty.call(this._blocks, block.id)) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// Create new block.
|
|
544
|
+
this._blocks[block.id] = block;
|
|
545
|
+
// Push block id to scripts array.
|
|
546
|
+
// Blocks are added as a top-level stack if they are marked as a top-block
|
|
547
|
+
// (if they were top-level XML in the event).
|
|
548
|
+
if (block.topLevel) {
|
|
549
|
+
this._addScript(block.id);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.resetCache();
|
|
553
|
+
|
|
554
|
+
// A new block was actually added to the block container,
|
|
555
|
+
// emit a project changed event
|
|
556
|
+
this.emitProjectChanged();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Block management: change block field values
|
|
561
|
+
* @param {!object} args Blockly change event to be processed
|
|
562
|
+
*/
|
|
563
|
+
changeBlock (args) {
|
|
564
|
+
// Validate
|
|
565
|
+
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
|
|
566
|
+
let block = this._blocks[args.id];
|
|
567
|
+
if (typeof block === 'undefined') return;
|
|
568
|
+
switch (args.element) {
|
|
569
|
+
case 'field':
|
|
570
|
+
// TODO when the field of a monitored block changes,
|
|
571
|
+
// update the checkbox in the flyout based on whether
|
|
572
|
+
// a monitor for that current combination of selected parameters exists
|
|
573
|
+
// e.g.
|
|
574
|
+
// 1. check (current [v year])
|
|
575
|
+
// 2. switch dropdown in flyout block to (current [v minute])
|
|
576
|
+
// 3. the checkbox should become unchecked if we're not already
|
|
577
|
+
// monitoring current minute
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
// Update block value
|
|
581
|
+
if (!block.fields[args.name]) return;
|
|
582
|
+
if (args.name === 'VARIABLE' || args.name === 'LIST' ||
|
|
583
|
+
args.name === 'BROADCAST_OPTION') {
|
|
584
|
+
// Get variable name using the id in args.value.
|
|
585
|
+
const variable = this.runtime.getEditingTarget().lookupVariableById(args.value);
|
|
586
|
+
if (variable) {
|
|
587
|
+
block.fields[args.name].value = variable.name;
|
|
588
|
+
block.fields[args.name].id = args.value;
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// Changing the value in a dropdown
|
|
592
|
+
block.fields[args.name].value = args.value;
|
|
593
|
+
|
|
594
|
+
// The selected item in the sensing of block menu needs to change based on the
|
|
595
|
+
// selected target. Set it to the first item in the menu list.
|
|
596
|
+
// TODO: (#1787)
|
|
597
|
+
if (block.opcode === 'sensing_of_object_menu') {
|
|
598
|
+
if (block.fields.OBJECT.value === '_stage_') {
|
|
599
|
+
this._blocks[block.parent].fields.PROPERTY.value = 'backdrop #';
|
|
600
|
+
} else {
|
|
601
|
+
this._blocks[block.parent].fields.PROPERTY.value = 'x position';
|
|
602
|
+
}
|
|
603
|
+
this.runtime.requestBlocksUpdate();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block;
|
|
607
|
+
if (flyoutBlock.isMonitored) {
|
|
608
|
+
this.runtime.requestUpdateMonitor(Map({
|
|
609
|
+
id: flyoutBlock.id,
|
|
610
|
+
params: this._getBlockParams(flyoutBlock)
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
case 'mutation':
|
|
616
|
+
block.mutation = mutationAdapter(args.value);
|
|
617
|
+
break;
|
|
618
|
+
case 'checkbox': {
|
|
619
|
+
// A checkbox usually has a one to one correspondence with the monitor
|
|
620
|
+
// block but in the case of monitored reporters that have arguments,
|
|
621
|
+
// map the old id to a new id, creating a new monitor block if necessary
|
|
622
|
+
if (block.fields && Object.keys(block.fields).length > 0 &&
|
|
623
|
+
block.opcode !== 'data_variable' && block.opcode !== 'data_listcontents') {
|
|
624
|
+
|
|
625
|
+
// This block has an argument which needs to get separated out into
|
|
626
|
+
// multiple monitor blocks with ids based on the selected argument
|
|
627
|
+
const newId = getMonitorIdForBlockWithArgs(block.id, block.fields);
|
|
628
|
+
// Note: we're not just constantly creating a longer and longer id everytime we check
|
|
629
|
+
// the checkbox because we're using the id of the block in the flyout as the base
|
|
630
|
+
|
|
631
|
+
// check if a block with the new id already exists, otherwise create
|
|
632
|
+
let newBlock = this.runtime.monitorBlocks.getBlock(newId);
|
|
633
|
+
if (!newBlock) {
|
|
634
|
+
newBlock = JSON.parse(JSON.stringify(block));
|
|
635
|
+
newBlock.id = newId;
|
|
636
|
+
this.runtime.monitorBlocks.createBlock(newBlock);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
block = newBlock; // Carry on through the rest of this code with newBlock
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const wasMonitored = block.isMonitored;
|
|
643
|
+
block.isMonitored = args.value;
|
|
644
|
+
|
|
645
|
+
// Variable blocks may be sprite specific depending on the owner of the variable
|
|
646
|
+
let isSpriteLocalVariable = false;
|
|
647
|
+
if (block.opcode === 'data_variable') {
|
|
648
|
+
isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.VARIABLE.id]);
|
|
649
|
+
} else if (block.opcode === 'data_listcontents') {
|
|
650
|
+
isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.LIST.id]);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const isSpriteSpecific = isSpriteLocalVariable ||
|
|
654
|
+
(Object.prototype.hasOwnProperty.call(this.runtime.monitorBlockInfo, block.opcode) &&
|
|
655
|
+
this.runtime.monitorBlockInfo[block.opcode].isSpriteSpecific);
|
|
656
|
+
if (isSpriteSpecific) {
|
|
657
|
+
// If creating a new sprite specific monitor, the only possible target is
|
|
658
|
+
// the current editing one b/c you cannot dynamically create monitors.
|
|
659
|
+
// Also, do not change the targetId if it has already been assigned
|
|
660
|
+
block.targetId = block.targetId || this.runtime.getEditingTarget().id;
|
|
661
|
+
} else {
|
|
662
|
+
block.targetId = null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (wasMonitored && !block.isMonitored) {
|
|
666
|
+
this.runtime.requestHideMonitor(block.id);
|
|
667
|
+
} else if (!wasMonitored && block.isMonitored) {
|
|
668
|
+
// Tries to show the monitor for specified block. If it doesn't exist, add the monitor.
|
|
669
|
+
if (!this.runtime.requestShowMonitor(block.id)) {
|
|
670
|
+
this.runtime.requestAddMonitor(MonitorRecord({
|
|
671
|
+
id: block.id,
|
|
672
|
+
targetId: block.targetId,
|
|
673
|
+
spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null,
|
|
674
|
+
opcode: block.opcode,
|
|
675
|
+
params: this._getBlockParams(block),
|
|
676
|
+
// @todo(vm#565) for numerical values with decimals, some countries use comma
|
|
677
|
+
value: '',
|
|
678
|
+
mode: block.opcode === 'data_listcontents' ? 'list' : 'default'
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this.emitProjectChanged();
|
|
687
|
+
|
|
688
|
+
this.resetCache();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Block management: move blocks from parent to parent
|
|
693
|
+
* @param {!object} e Blockly move event to be processed
|
|
694
|
+
*/
|
|
695
|
+
moveBlock (e) {
|
|
696
|
+
if (!Object.prototype.hasOwnProperty.call(this._blocks, e.id)) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const block = this._blocks[e.id];
|
|
701
|
+
// Track whether a change actually occurred
|
|
702
|
+
// ignoring changes like routine re-positioning
|
|
703
|
+
// of a block when loading a workspace
|
|
704
|
+
let didChange = false;
|
|
705
|
+
|
|
706
|
+
// Move coordinate changes.
|
|
707
|
+
if (e.newCoordinate) {
|
|
708
|
+
|
|
709
|
+
didChange = (block.x !== e.newCoordinate.x) || (block.y !== e.newCoordinate.y);
|
|
710
|
+
|
|
711
|
+
block.x = e.newCoordinate.x;
|
|
712
|
+
block.y = e.newCoordinate.y;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Remove from any old parent.
|
|
716
|
+
if (typeof e.oldParent !== 'undefined') {
|
|
717
|
+
const oldParent = this._blocks[e.oldParent];
|
|
718
|
+
if (typeof e.oldInput !== 'undefined' &&
|
|
719
|
+
oldParent.inputs[e.oldInput].block === e.id) {
|
|
720
|
+
// This block was connected to the old parent's input.
|
|
721
|
+
oldParent.inputs[e.oldInput].block = null;
|
|
722
|
+
} else if (oldParent.next === e.id) {
|
|
723
|
+
// This block was connected to the old parent's next connection.
|
|
724
|
+
oldParent.next = null;
|
|
725
|
+
}
|
|
726
|
+
this._blocks[e.id].parent = null;
|
|
727
|
+
didChange = true;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Is this block a top-level block?
|
|
731
|
+
if (typeof e.newParent === 'undefined') {
|
|
732
|
+
this._addScript(e.id);
|
|
733
|
+
} else {
|
|
734
|
+
// Remove script, if one exists.
|
|
735
|
+
this._deleteScript(e.id);
|
|
736
|
+
// Otherwise, try to connect it in its new place.
|
|
737
|
+
if (typeof e.newInput === 'undefined') {
|
|
738
|
+
// Moved to the new parent's next connection.
|
|
739
|
+
this._blocks[e.newParent].next = e.id;
|
|
740
|
+
} else {
|
|
741
|
+
// Moved to the new parent's input.
|
|
742
|
+
// Don't obscure the shadow block.
|
|
743
|
+
let oldShadow = null;
|
|
744
|
+
if (Object.prototype.hasOwnProperty.call(this._blocks[e.newParent].inputs, e.newInput)) {
|
|
745
|
+
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// If the block being attached is itself a shadow, make sure to set
|
|
749
|
+
// both block and shadow to that blocks ID. This happens when adding
|
|
750
|
+
// inputs to a custom procedure.
|
|
751
|
+
if (this._blocks[e.id].shadow) oldShadow = e.id;
|
|
752
|
+
|
|
753
|
+
this._blocks[e.newParent].inputs[e.newInput] = {
|
|
754
|
+
name: e.newInput,
|
|
755
|
+
block: e.id,
|
|
756
|
+
shadow: oldShadow
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
this._blocks[e.id].parent = e.newParent;
|
|
760
|
+
didChange = true;
|
|
761
|
+
}
|
|
762
|
+
this.resetCache();
|
|
763
|
+
|
|
764
|
+
if (didChange) this.emitProjectChanged();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Block management: run all blocks.
|
|
770
|
+
* @param {!object} runtime Runtime to run all blocks in.
|
|
771
|
+
*/
|
|
772
|
+
runAllMonitored (runtime) {
|
|
773
|
+
if (this._cache._monitored === null) {
|
|
774
|
+
this._cache._monitored = Object.keys(this._blocks)
|
|
775
|
+
.filter(blockId => this.getBlock(blockId).isMonitored)
|
|
776
|
+
.map(blockId => {
|
|
777
|
+
const targetId = this.getBlock(blockId).targetId;
|
|
778
|
+
return {
|
|
779
|
+
blockId,
|
|
780
|
+
target: targetId ? runtime.getTargetById(targetId) : null
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const monitored = this._cache._monitored;
|
|
786
|
+
for (let i = 0; i < monitored.length; i++) {
|
|
787
|
+
const {blockId, target} = monitored[i];
|
|
788
|
+
runtime.addMonitorScript(blockId, target);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Block management: delete blocks and their associated scripts. Does nothing if a block
|
|
794
|
+
* with the given ID does not exist.
|
|
795
|
+
* @param {!string} blockId Id of block to delete
|
|
796
|
+
*/
|
|
797
|
+
deleteBlock (blockId) {
|
|
798
|
+
// @todo In runtime, stop threads running on this script.
|
|
799
|
+
|
|
800
|
+
// Get block
|
|
801
|
+
const block = this._blocks[blockId];
|
|
802
|
+
if (!block) {
|
|
803
|
+
// No block with the given ID exists
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Delete children
|
|
808
|
+
if (block.next !== null) {
|
|
809
|
+
this.deleteBlock(block.next);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Delete inputs (including branches)
|
|
813
|
+
for (const input in block.inputs) {
|
|
814
|
+
// If it's null, the block in this input moved away.
|
|
815
|
+
if (block.inputs[input].block !== null) {
|
|
816
|
+
this.deleteBlock(block.inputs[input].block);
|
|
817
|
+
}
|
|
818
|
+
// Delete obscured shadow blocks.
|
|
819
|
+
if (block.inputs[input].shadow !== null &&
|
|
820
|
+
block.inputs[input].shadow !== block.inputs[input].block) {
|
|
821
|
+
this.deleteBlock(block.inputs[input].shadow);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Delete any script starting with this block.
|
|
826
|
+
this._deleteScript(blockId);
|
|
827
|
+
|
|
828
|
+
// Delete block itself.
|
|
829
|
+
delete this._blocks[blockId];
|
|
830
|
+
|
|
831
|
+
this.resetCache();
|
|
832
|
+
this.emitProjectChanged();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Delete all blocks and their associated scripts.
|
|
837
|
+
*/
|
|
838
|
+
deleteAllBlocks () {
|
|
839
|
+
const blockIds = Object.keys(this._blocks);
|
|
840
|
+
blockIds.forEach(blockId => this.deleteBlock(blockId));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Returns a map of all references to variables or lists from blocks
|
|
845
|
+
* in this block container.
|
|
846
|
+
* @param {Array<object>} optBlocks Optional list of blocks to constrain the search to.
|
|
847
|
+
* This is useful for getting variable/list references for a stack of blocks instead
|
|
848
|
+
* of all blocks on the workspace
|
|
849
|
+
* @param {?boolean} optIncludeBroadcast Optional whether to include broadcast fields.
|
|
850
|
+
* @return {object} A map of variable ID to a list of all variable references
|
|
851
|
+
* for that ID. A variable reference contains the field referencing that variable
|
|
852
|
+
* and also the type of the variable being referenced.
|
|
853
|
+
*/
|
|
854
|
+
getAllVariableAndListReferences (optBlocks, optIncludeBroadcast) {
|
|
855
|
+
const blocks = optBlocks ? optBlocks : this._blocks;
|
|
856
|
+
const allReferences = Object.create(null);
|
|
857
|
+
for (const blockId in blocks) {
|
|
858
|
+
let varOrListField = null;
|
|
859
|
+
let varType = null;
|
|
860
|
+
if (blocks[blockId].fields.VARIABLE) {
|
|
861
|
+
varOrListField = blocks[blockId].fields.VARIABLE;
|
|
862
|
+
varType = Variable.SCALAR_TYPE;
|
|
863
|
+
} else if (blocks[blockId].fields.LIST) {
|
|
864
|
+
varOrListField = blocks[blockId].fields.LIST;
|
|
865
|
+
varType = Variable.LIST_TYPE;
|
|
866
|
+
} else if (optIncludeBroadcast && blocks[blockId].fields.BROADCAST_OPTION) {
|
|
867
|
+
varOrListField = blocks[blockId].fields.BROADCAST_OPTION;
|
|
868
|
+
varType = Variable.BROADCAST_MESSAGE_TYPE;
|
|
869
|
+
}
|
|
870
|
+
if (varOrListField) {
|
|
871
|
+
const currVarId = varOrListField.id;
|
|
872
|
+
if (allReferences[currVarId]) {
|
|
873
|
+
allReferences[currVarId].push({
|
|
874
|
+
referencingField: varOrListField,
|
|
875
|
+
type: varType
|
|
876
|
+
});
|
|
877
|
+
} else {
|
|
878
|
+
allReferences[currVarId] = [{
|
|
879
|
+
referencingField: varOrListField,
|
|
880
|
+
type: varType
|
|
881
|
+
}];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return allReferences;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Keep blocks up to date after a variable gets renamed.
|
|
890
|
+
* @param {string} varId The id of the variable that was renamed
|
|
891
|
+
* @param {string} newName The new name of the variable that was renamed
|
|
892
|
+
*/
|
|
893
|
+
updateBlocksAfterVarRename (varId, newName) {
|
|
894
|
+
const blocks = this._blocks;
|
|
895
|
+
for (const blockId in blocks) {
|
|
896
|
+
let varOrListField = null;
|
|
897
|
+
if (blocks[blockId].fields.VARIABLE) {
|
|
898
|
+
varOrListField = blocks[blockId].fields.VARIABLE;
|
|
899
|
+
} else if (blocks[blockId].fields.LIST) {
|
|
900
|
+
varOrListField = blocks[blockId].fields.LIST;
|
|
901
|
+
}
|
|
902
|
+
if (varOrListField) {
|
|
903
|
+
const currFieldId = varOrListField.id;
|
|
904
|
+
if (varId === currFieldId) {
|
|
905
|
+
varOrListField.value = newName;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Keep blocks up to date after they are shared between targets.
|
|
913
|
+
* @param {boolean} isStage If the new target is a stage.
|
|
914
|
+
*/
|
|
915
|
+
updateTargetSpecificBlocks (isStage) {
|
|
916
|
+
const blocks = this._blocks;
|
|
917
|
+
for (const blockId in blocks) {
|
|
918
|
+
if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') {
|
|
919
|
+
blocks[blockId].opcode = 'event_whenstageclicked';
|
|
920
|
+
} else if (!isStage && blocks[blockId].opcode === 'event_whenstageclicked') {
|
|
921
|
+
blocks[blockId].opcode = 'event_whenthisspriteclicked';
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Update blocks after a sound, costume, or backdrop gets renamed.
|
|
928
|
+
* Any block referring to the old name of the asset should get updated
|
|
929
|
+
* to refer to the new name.
|
|
930
|
+
* @param {string} oldName The old name of the asset that was renamed.
|
|
931
|
+
* @param {string} newName The new name of the asset that was renamed.
|
|
932
|
+
* @param {string} assetType String representation of the kind of asset
|
|
933
|
+
* that was renamed. This can be one of 'sprite','costume', 'sound', or
|
|
934
|
+
* 'backdrop'.
|
|
935
|
+
*/
|
|
936
|
+
updateAssetName (oldName, newName, assetType) {
|
|
937
|
+
let getAssetField;
|
|
938
|
+
if (assetType === 'costume') {
|
|
939
|
+
getAssetField = this._getCostumeField.bind(this);
|
|
940
|
+
} else if (assetType === 'sound') {
|
|
941
|
+
getAssetField = this._getSoundField.bind(this);
|
|
942
|
+
} else if (assetType === 'backdrop') {
|
|
943
|
+
getAssetField = this._getBackdropField.bind(this);
|
|
944
|
+
} else if (assetType === 'sprite') {
|
|
945
|
+
getAssetField = this._getSpriteField.bind(this);
|
|
946
|
+
} else {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const blocks = this._blocks;
|
|
950
|
+
for (const blockId in blocks) {
|
|
951
|
+
const assetField = getAssetField(blockId);
|
|
952
|
+
if (assetField && assetField.value === oldName) {
|
|
953
|
+
assetField.value = newName;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Update sensing_of blocks after a variable gets renamed.
|
|
960
|
+
* @param {string} oldName The old name of the variable that was renamed.
|
|
961
|
+
* @param {string} newName The new name of the variable that was renamed.
|
|
962
|
+
* @param {string} targetName The name of the target the variable belongs to.
|
|
963
|
+
* @return {boolean} Returns true if any of the blocks were updated.
|
|
964
|
+
*/
|
|
965
|
+
updateSensingOfReference (oldName, newName, targetName) {
|
|
966
|
+
const blocks = this._blocks;
|
|
967
|
+
let blockUpdated = false;
|
|
968
|
+
for (const blockId in blocks) {
|
|
969
|
+
const block = blocks[blockId];
|
|
970
|
+
if (block.opcode === 'sensing_of' &&
|
|
971
|
+
block.fields.PROPERTY.value === oldName &&
|
|
972
|
+
// If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored.
|
|
973
|
+
block.inputs.OBJECT.block === block.inputs.OBJECT.shadow) {
|
|
974
|
+
const inputBlock = this.getBlock(block.inputs.OBJECT.block);
|
|
975
|
+
if (inputBlock.fields.OBJECT.value === targetName) {
|
|
976
|
+
block.fields.PROPERTY.value = newName;
|
|
977
|
+
blockUpdated = true;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (blockUpdated) this.resetCache();
|
|
982
|
+
return blockUpdated;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Helper function to retrieve a costume menu field from a block given its id.
|
|
987
|
+
* @param {string} blockId A unique identifier for a block
|
|
988
|
+
* @return {?object} The costume menu field of the block with the given block id.
|
|
989
|
+
* Null if either a block with the given id doesn't exist or if a costume menu field
|
|
990
|
+
* does not exist on the block with the given id.
|
|
991
|
+
*/
|
|
992
|
+
_getCostumeField (blockId) {
|
|
993
|
+
const block = this.getBlock(blockId);
|
|
994
|
+
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')) {
|
|
995
|
+
return block.fields.COSTUME;
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Helper function to retrieve a sound menu field from a block given its id.
|
|
1002
|
+
* @param {string} blockId A unique identifier for a block
|
|
1003
|
+
* @return {?object} The sound menu field of the block with the given block id.
|
|
1004
|
+
* Null, if either a block with the given id doesn't exist or if a sound menu field
|
|
1005
|
+
* does not exist on the block with the given id.
|
|
1006
|
+
*/
|
|
1007
|
+
_getSoundField (blockId) {
|
|
1008
|
+
const block = this.getBlock(blockId);
|
|
1009
|
+
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')) {
|
|
1010
|
+
return block.fields.SOUND_MENU;
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Helper function to retrieve a backdrop menu field from a block given its id.
|
|
1017
|
+
* @param {string} blockId A unique identifier for a block
|
|
1018
|
+
* @return {?object} The backdrop menu field of the block with the given block id.
|
|
1019
|
+
* Null, if either a block with the given id doesn't exist or if a backdrop menu field
|
|
1020
|
+
* does not exist on the block with the given id.
|
|
1021
|
+
*/
|
|
1022
|
+
_getBackdropField (blockId) {
|
|
1023
|
+
const block = this.getBlock(blockId);
|
|
1024
|
+
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')) {
|
|
1025
|
+
return block.fields.BACKDROP;
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Helper function to retrieve a sprite menu field from a block given its id.
|
|
1032
|
+
* @param {string} blockId A unique identifier for a block
|
|
1033
|
+
* @return {?object} The sprite menu field of the block with the given block id.
|
|
1034
|
+
* Null, if either a block with the given id doesn't exist or if a sprite menu field
|
|
1035
|
+
* does not exist on the block with the given id.
|
|
1036
|
+
*/
|
|
1037
|
+
_getSpriteField (blockId) {
|
|
1038
|
+
const block = this.getBlock(blockId);
|
|
1039
|
+
if (!block) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
const spriteMenuNames = ['TOWARDS', 'TO', 'OBJECT', 'VIDEOONMENU2',
|
|
1043
|
+
'DISTANCETOMENU', 'TOUCHINGOBJECTMENU', 'CLONE_OPTION'];
|
|
1044
|
+
for (let i = 0; i < spriteMenuNames.length; i++) {
|
|
1045
|
+
const menuName = spriteMenuNames[i];
|
|
1046
|
+
if (Object.prototype.hasOwnProperty.call(block.fields, menuName)) {
|
|
1047
|
+
return block.fields[menuName];
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ---------------------------------------------------------------------
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Encode all of `this._blocks` as an XML string usable
|
|
1057
|
+
* by a Blockly/scratch-blocks workspace.
|
|
1058
|
+
* @param {object<string, Comment>} comments Map of comments referenced by id
|
|
1059
|
+
* @return {string} String of XML representing this object's blocks.
|
|
1060
|
+
*/
|
|
1061
|
+
toXML (comments) {
|
|
1062
|
+
return this._scripts.map(script => this.blockToXML(script, comments)).join();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Recursively encode an individual block and its children
|
|
1067
|
+
* into a Blockly/scratch-blocks XML string.
|
|
1068
|
+
* @param {!string} blockId ID of block to encode.
|
|
1069
|
+
* @param {object<string, Comment>} comments Map of comments referenced by id
|
|
1070
|
+
* @return {string} String of XML representing this block and any children.
|
|
1071
|
+
*/
|
|
1072
|
+
blockToXML (blockId, comments) {
|
|
1073
|
+
const block = this._blocks[blockId];
|
|
1074
|
+
// block should exist, but currently some blocks' next property point
|
|
1075
|
+
// to a blockId for non-existent blocks. Until we track down that behavior,
|
|
1076
|
+
// this early exit allows the project to load.
|
|
1077
|
+
if (!block) return;
|
|
1078
|
+
// Encode properties of this block.
|
|
1079
|
+
const tagName = (block.shadow) ? 'shadow' : 'block';
|
|
1080
|
+
let xmlString =
|
|
1081
|
+
`<${tagName}
|
|
1082
|
+
id="${block.id}"
|
|
1083
|
+
type="${block.opcode}"
|
|
1084
|
+
${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''}
|
|
1085
|
+
>`;
|
|
1086
|
+
const commentId = block.comment;
|
|
1087
|
+
if (commentId) {
|
|
1088
|
+
if (comments) {
|
|
1089
|
+
if (Object.prototype.hasOwnProperty.call(comments, commentId)) {
|
|
1090
|
+
xmlString += comments[commentId].toXML();
|
|
1091
|
+
} else {
|
|
1092
|
+
log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`);
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
// Add any mutation. Must come before inputs.
|
|
1099
|
+
if (block.mutation) {
|
|
1100
|
+
xmlString += this.mutationToXML(block.mutation);
|
|
1101
|
+
}
|
|
1102
|
+
// Add any inputs on this block.
|
|
1103
|
+
for (const input in block.inputs) {
|
|
1104
|
+
if (!Object.prototype.hasOwnProperty.call(block.inputs, input)) continue;
|
|
1105
|
+
const blockInput = block.inputs[input];
|
|
1106
|
+
// Only encode a value tag if the value input is occupied.
|
|
1107
|
+
if (blockInput.block || blockInput.shadow) {
|
|
1108
|
+
xmlString += `<value name="${blockInput.name}">`;
|
|
1109
|
+
if (blockInput.block) {
|
|
1110
|
+
xmlString += this.blockToXML(blockInput.block, comments);
|
|
1111
|
+
}
|
|
1112
|
+
if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
|
|
1113
|
+
// Obscured shadow.
|
|
1114
|
+
xmlString += this.blockToXML(blockInput.shadow, comments);
|
|
1115
|
+
}
|
|
1116
|
+
xmlString += '</value>';
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// Add any fields on this block.
|
|
1120
|
+
for (const field in block.fields) {
|
|
1121
|
+
if (!Object.prototype.hasOwnProperty.call(block.fields, field)) continue;
|
|
1122
|
+
const blockField = block.fields[field];
|
|
1123
|
+
xmlString += `<field name="${blockField.name}"`;
|
|
1124
|
+
const fieldId = blockField.id;
|
|
1125
|
+
if (fieldId) {
|
|
1126
|
+
xmlString += ` id="${fieldId}"`;
|
|
1127
|
+
}
|
|
1128
|
+
const varType = blockField.variableType;
|
|
1129
|
+
if (typeof varType === 'string') {
|
|
1130
|
+
xmlString += ` variabletype="${varType}"`;
|
|
1131
|
+
}
|
|
1132
|
+
let value = blockField.value;
|
|
1133
|
+
if (typeof value === 'string') {
|
|
1134
|
+
value = xmlEscape(blockField.value);
|
|
1135
|
+
}
|
|
1136
|
+
xmlString += `>${value}</field>`;
|
|
1137
|
+
}
|
|
1138
|
+
// Add blocks connected to the next connection.
|
|
1139
|
+
if (block.next) {
|
|
1140
|
+
xmlString += `<next>${this.blockToXML(block.next, comments)}</next>`;
|
|
1141
|
+
}
|
|
1142
|
+
xmlString += `</${tagName}>`;
|
|
1143
|
+
return xmlString;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Recursively encode a mutation object to XML.
|
|
1148
|
+
* @param {!object} mutation Object representing a mutation.
|
|
1149
|
+
* @return {string} XML string representing a mutation.
|
|
1150
|
+
*/
|
|
1151
|
+
mutationToXML (mutation) {
|
|
1152
|
+
let mutationString = `<${mutation.tagName}`;
|
|
1153
|
+
for (const prop in mutation) {
|
|
1154
|
+
if (prop === 'children' || prop === 'tagName') continue;
|
|
1155
|
+
let mutationValue = (typeof mutation[prop] === 'string') ?
|
|
1156
|
+
xmlEscape(mutation[prop]) : mutation[prop];
|
|
1157
|
+
|
|
1158
|
+
// Handle dynamic extension blocks
|
|
1159
|
+
if (prop === 'blockInfo') {
|
|
1160
|
+
mutationValue = xmlEscape(JSON.stringify(mutation[prop]));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
mutationString += ` ${prop}="${mutationValue}"`;
|
|
1164
|
+
}
|
|
1165
|
+
mutationString += '>';
|
|
1166
|
+
for (let i = 0; i < mutation.children.length; i++) {
|
|
1167
|
+
mutationString += this.mutationToXML(mutation.children[i]);
|
|
1168
|
+
}
|
|
1169
|
+
mutationString += `</${mutation.tagName}>`;
|
|
1170
|
+
return mutationString;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// ---------------------------------------------------------------------
|
|
1174
|
+
/**
|
|
1175
|
+
* Helper to serialize block fields and input fields for reporting new monitors
|
|
1176
|
+
* @param {!object} block Block to be paramified.
|
|
1177
|
+
* @return {!object} object of param key/values.
|
|
1178
|
+
*/
|
|
1179
|
+
_getBlockParams (block) {
|
|
1180
|
+
const params = {};
|
|
1181
|
+
for (const key in block.fields) {
|
|
1182
|
+
params[key] = block.fields[key].value;
|
|
1183
|
+
}
|
|
1184
|
+
for (const inputKey in block.inputs) {
|
|
1185
|
+
const inputBlock = this._blocks[block.inputs[inputKey].block];
|
|
1186
|
+
for (const key in inputBlock.fields) {
|
|
1187
|
+
params[key] = inputBlock.fields[key].value;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return params;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Helper to get the corresponding internal procedure definition block
|
|
1195
|
+
* @param {!object} defineBlock Outer define block.
|
|
1196
|
+
* @return {!object} internal definition block which has the mutation.
|
|
1197
|
+
*/
|
|
1198
|
+
_getCustomBlockInternal (defineBlock) {
|
|
1199
|
+
if (defineBlock.inputs && defineBlock.inputs.custom_block) {
|
|
1200
|
+
return this._blocks[defineBlock.inputs.custom_block.block];
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Helper to add a stack to `this._scripts`.
|
|
1206
|
+
* @param {?string} topBlockId ID of block that starts the script.
|
|
1207
|
+
*/
|
|
1208
|
+
_addScript (topBlockId) {
|
|
1209
|
+
const i = this._scripts.indexOf(topBlockId);
|
|
1210
|
+
if (i > -1) return; // Already in scripts.
|
|
1211
|
+
this._scripts.push(topBlockId);
|
|
1212
|
+
// Update `topLevel` property on the top block.
|
|
1213
|
+
this._blocks[topBlockId].topLevel = true;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Helper to remove a script from `this._scripts`.
|
|
1218
|
+
* @param {?string} topBlockId ID of block that starts the script.
|
|
1219
|
+
*/
|
|
1220
|
+
_deleteScript (topBlockId) {
|
|
1221
|
+
const i = this._scripts.indexOf(topBlockId);
|
|
1222
|
+
if (i > -1) this._scripts.splice(i, 1);
|
|
1223
|
+
// Update `topLevel` property on the top block.
|
|
1224
|
+
if (this._blocks[topBlockId]) this._blocks[topBlockId].topLevel = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* A private method shared with execute to build an object containing the block
|
|
1230
|
+
* information execute needs and that is reset when other cached Blocks info is
|
|
1231
|
+
* reset.
|
|
1232
|
+
* @param {Blocks} blocks Blocks containing the expected blockId
|
|
1233
|
+
* @param {string} blockId blockId for the desired execute cache
|
|
1234
|
+
* @param {function} CacheType constructor for cached block information
|
|
1235
|
+
* @return {object} execute cache object
|
|
1236
|
+
*/
|
|
1237
|
+
BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) {
|
|
1238
|
+
let cached = blocks._cache._executeCached[blockId];
|
|
1239
|
+
if (typeof cached !== 'undefined') {
|
|
1240
|
+
return cached;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const block = blocks.getBlock(blockId);
|
|
1244
|
+
if (typeof block === 'undefined') return null;
|
|
1245
|
+
|
|
1246
|
+
if (typeof CacheType === 'undefined') {
|
|
1247
|
+
cached = {
|
|
1248
|
+
id: blockId,
|
|
1249
|
+
opcode: blocks.getOpcode(block),
|
|
1250
|
+
fields: blocks.getFields(block),
|
|
1251
|
+
inputs: blocks.getInputs(block),
|
|
1252
|
+
mutation: blocks.getMutation(block)
|
|
1253
|
+
};
|
|
1254
|
+
} else {
|
|
1255
|
+
cached = new CacheType(blocks, {
|
|
1256
|
+
id: blockId,
|
|
1257
|
+
opcode: blocks.getOpcode(block),
|
|
1258
|
+
fields: blocks.getFields(block),
|
|
1259
|
+
inputs: blocks.getInputs(block),
|
|
1260
|
+
mutation: blocks.getMutation(block)
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
blocks._cache._executeCached[blockId] = cached;
|
|
1265
|
+
return cached;
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Cache class constructor for runtime. Used to consider what threads should
|
|
1270
|
+
* start based on hat data.
|
|
1271
|
+
* @type {function}
|
|
1272
|
+
*/
|
|
1273
|
+
const RuntimeScriptCache = BlocksRuntimeCache._RuntimeScriptCache;
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Get an array of scripts from a block container prefiltered to match opcode.
|
|
1277
|
+
* @param {Blocks} blocks - Container of blocks
|
|
1278
|
+
* @param {string} opcode - Opcode to filter top blocks by
|
|
1279
|
+
* @returns {Array.<RuntimeScriptCache>} - Array of RuntimeScriptCache cache
|
|
1280
|
+
* objects
|
|
1281
|
+
*/
|
|
1282
|
+
BlocksRuntimeCache.getScripts = function (blocks, opcode) {
|
|
1283
|
+
let scripts = blocks._cache.scripts[opcode];
|
|
1284
|
+
if (!scripts) {
|
|
1285
|
+
scripts = blocks._cache.scripts[opcode] = [];
|
|
1286
|
+
|
|
1287
|
+
const allScripts = blocks._scripts;
|
|
1288
|
+
for (let i = 0; i < allScripts.length; i++) {
|
|
1289
|
+
const topBlockId = allScripts[i];
|
|
1290
|
+
const block = blocks.getBlock(topBlockId);
|
|
1291
|
+
if (block.opcode === opcode) {
|
|
1292
|
+
scripts.push(new RuntimeScriptCache(blocks, topBlockId));
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return scripts;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
module.exports = Blocks;
|