mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-25 07:22:33 -05:00
Merge branch 'develop' into fix-say-flickering
This commit is contained in:
commit
e4c109c0c8
12 changed files with 286 additions and 291 deletions
|
@ -144,15 +144,21 @@ class Scratch3ControlBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
createClone (args, util) {
|
createClone (args, util) {
|
||||||
|
// Cast argument to string
|
||||||
|
args.CLONE_OPTION = Cast.toString(args.CLONE_OPTION);
|
||||||
|
|
||||||
|
// Set clone target
|
||||||
let cloneTarget;
|
let cloneTarget;
|
||||||
if (args.CLONE_OPTION === '_myself_') {
|
if (args.CLONE_OPTION === '_myself_') {
|
||||||
cloneTarget = util.target;
|
cloneTarget = util.target;
|
||||||
} else {
|
} else {
|
||||||
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
|
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
|
||||||
}
|
}
|
||||||
if (!cloneTarget) {
|
|
||||||
return;
|
// If clone target is not found, return
|
||||||
}
|
if (!cloneTarget) return;
|
||||||
|
|
||||||
|
// Create clone
|
||||||
const newClone = cloneTarget.makeClone();
|
const newClone = cloneTarget.makeClone();
|
||||||
if (newClone) {
|
if (newClone) {
|
||||||
this.runtime.targets.push(newClone);
|
this.runtime.targets.push(newClone);
|
||||||
|
|
|
@ -11,6 +11,8 @@ const uid = require('../util/uid');
|
||||||
* See _renderBubble for explanation of this optimization.
|
* See _renderBubble for explanation of this optimization.
|
||||||
* @property {string} text - the text of the bubble.
|
* @property {string} text - the text of the bubble.
|
||||||
* @property {string} type - the type of the bubble, "say" or "think"
|
* @property {string} type - the type of the bubble, "say" or "think"
|
||||||
|
* @property {?string} usageId - ID indicating the most recent usage of the say/think bubble.
|
||||||
|
* Used for comparison when determining whether to clear a say/think bubble.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Scratch3LooksBlocks {
|
class Scratch3LooksBlocks {
|
||||||
|
@ -46,7 +48,7 @@ class Scratch3LooksBlocks {
|
||||||
skinId: null,
|
skinId: null,
|
||||||
text: '',
|
text: '',
|
||||||
type: 'say',
|
type: 'say',
|
||||||
usageId: null // ID for hiding a timed say/think block.
|
usageId: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
src/engine/blocks-execute-cache.js
Normal file
19
src/engine/blocks-execute-cache.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* Access point for private method shared between blocks.js and execute.js for
|
||||||
|
* caching execute information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private method shared with execute to build an object containing the block
|
||||||
|
* information execute needs and that is reset when other cached Blocks info is
|
||||||
|
* reset.
|
||||||
|
* @param {Blocks} blocks Blocks containing the expected blockId
|
||||||
|
* @param {string} blockId blockId for the desired execute cache
|
||||||
|
*/
|
||||||
|
exports.getCached = function () {
|
||||||
|
throw new Error('blocks.js has not initialized BlocksExecuteCache');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call after the default throwing getCached is assigned for Blocks to replace.
|
||||||
|
require('./blocks');
|
|
@ -4,6 +4,7 @@ const xmlEscape = require('../util/xml-escape');
|
||||||
const MonitorRecord = require('./monitor-record');
|
const MonitorRecord = require('./monitor-record');
|
||||||
const Clone = require('../util/clone');
|
const Clone = require('../util/clone');
|
||||||
const {Map} = require('immutable');
|
const {Map} = require('immutable');
|
||||||
|
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -47,7 +48,14 @@ class Blocks {
|
||||||
* Cache procedure definitions by block id
|
* Cache procedure definitions by block id
|
||||||
* @type {object.<string, ?string>}
|
* @type {object.<string, ?string>}
|
||||||
*/
|
*/
|
||||||
procedureDefinitions: {}
|
procedureDefinitions: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache for execute to use and store on. Only available to
|
||||||
|
* execute.
|
||||||
|
* @type {object.<string, object>}
|
||||||
|
*/
|
||||||
|
_executeCached: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -359,6 +367,7 @@ class Blocks {
|
||||||
this._cache.inputs = {};
|
this._cache.inputs = {};
|
||||||
this._cache.procedureParamNames = {};
|
this._cache.procedureParamNames = {};
|
||||||
this._cache.procedureDefinitions = {};
|
this._cache.procedureDefinitions = {};
|
||||||
|
this._cache._executeCached = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -859,4 +868,31 @@ class Blocks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private method shared with execute to build an object containing the block
|
||||||
|
* information execute needs and that is reset when other cached Blocks info is
|
||||||
|
* reset.
|
||||||
|
* @param {Blocks} blocks Blocks containing the expected blockId
|
||||||
|
* @param {string} blockId blockId for the desired execute cache
|
||||||
|
* @return {object} execute cache object
|
||||||
|
*/
|
||||||
|
BlocksExecuteCache.getCached = function (blocks, blockId) {
|
||||||
|
const block = blocks.getBlock(blockId);
|
||||||
|
if (typeof block === 'undefined') return null;
|
||||||
|
let cached = blocks._cache._executeCached[blockId];
|
||||||
|
if (typeof cached !== 'undefined') {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
cached = {
|
||||||
|
_initialized: false,
|
||||||
|
opcode: blocks.getOpcode(block),
|
||||||
|
fields: blocks.getFields(block),
|
||||||
|
inputs: blocks.getInputs(block),
|
||||||
|
mutation: blocks.getMutation(block)
|
||||||
|
};
|
||||||
|
blocks._cache._executeCached[blockId] = cached;
|
||||||
|
return cached;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = Blocks;
|
module.exports = Blocks;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const BlockUtility = require('./block-utility');
|
const BlockUtility = require('./block-utility');
|
||||||
|
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
const Thread = require('./thread');
|
const Thread = require('./thread');
|
||||||
const {Map} = require('immutable');
|
const {Map} = require('immutable');
|
||||||
|
@ -98,32 +99,39 @@ const handleReport = function (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience constant to help make use of the recursiveCall argument easier
|
||||||
|
* to read.
|
||||||
|
* @const {boolean}
|
||||||
|
*/
|
||||||
|
const RECURSIVE = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple description of the kind of information held in the fields of a block.
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const FieldKind = {
|
||||||
|
NONE: 'NONE',
|
||||||
|
VARIABLE: 'VARIABLE',
|
||||||
|
LIST: 'LIST',
|
||||||
|
BROADCAST_OPTION: 'BROADCAST_OPTION',
|
||||||
|
DYNAMIC: 'DYNAMIC'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a block.
|
* Execute a block.
|
||||||
* @param {!Sequencer} sequencer Which sequencer is executing.
|
* @param {!Sequencer} sequencer Which sequencer is executing.
|
||||||
* @param {!Thread} thread Thread which to read and execute.
|
* @param {!Thread} thread Thread which to read and execute.
|
||||||
|
* @param {boolean} recursiveCall is execute called from another execute call?
|
||||||
*/
|
*/
|
||||||
const execute = function (sequencer, thread) {
|
const execute = function (sequencer, thread, recursiveCall) {
|
||||||
const runtime = sequencer.runtime;
|
const runtime = sequencer.runtime;
|
||||||
const target = thread.target;
|
|
||||||
|
|
||||||
// Stop if block or target no longer exists.
|
|
||||||
if (target === null) {
|
|
||||||
// No block found: stop the thread; script no longer exists.
|
|
||||||
sequencer.retireThread(thread);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current block to execute is the one on the top of the stack.
|
// Current block to execute is the one on the top of the stack.
|
||||||
const currentBlockId = thread.peekStack();
|
const currentBlockId = thread.peekStack();
|
||||||
const currentStackFrame = thread.peekStackFrame();
|
const currentStackFrame = thread.peekStackFrame();
|
||||||
|
|
||||||
let blockContainer;
|
let blockContainer = thread.blockContainer;
|
||||||
if (thread.updateMonitor) {
|
|
||||||
blockContainer = runtime.monitorBlocks;
|
|
||||||
} else {
|
|
||||||
blockContainer = target.blocks;
|
|
||||||
}
|
|
||||||
let block = blockContainer.getBlock(currentBlockId);
|
let block = blockContainer.getBlock(currentBlockId);
|
||||||
if (typeof block === 'undefined') {
|
if (typeof block === 'undefined') {
|
||||||
blockContainer = runtime.flyoutBlocks;
|
blockContainer = runtime.flyoutBlocks;
|
||||||
|
@ -136,33 +144,94 @@ const execute = function (sequencer, thread) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const opcode = blockContainer.getOpcode(block);
|
// With the help of the Blocks class create a cached copy of values from
|
||||||
const fields = blockContainer.getFields(block);
|
// Blocks and the derived values execute needs. These values can be produced
|
||||||
const inputs = blockContainer.getInputs(block);
|
// one time during the first execution of a block so that later executions
|
||||||
const blockFunction = runtime.getOpcodeFunction(opcode);
|
// are faster by using these cached values. This helps turn most costly
|
||||||
const isHat = runtime.getIsHat(opcode);
|
// javascript operations like testing if the fields for a block has a
|
||||||
|
// certain key like VARIABLE into a test that done once is saved on the
|
||||||
|
// cache object to _isFieldVariable. This reduces the cost later in execute
|
||||||
|
// when that will determine how execute creates the argValues for the
|
||||||
|
// blockFunction.
|
||||||
|
//
|
||||||
|
// With Blocks providing this private function for execute to use, any time
|
||||||
|
// Blocks is modified in the editor these cached objects will be cleaned up
|
||||||
|
// and new cached copies can be created. This lets us optimize this critical
|
||||||
|
// path while keeping up to date with editor changes to a project.
|
||||||
|
const blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId);
|
||||||
|
if (blockCached._initialized !== true) {
|
||||||
|
const {opcode, fields, inputs} = blockCached;
|
||||||
|
|
||||||
|
// Assign opcode isHat and blockFunction data to avoid dynamic lookups.
|
||||||
|
blockCached._isHat = runtime.getIsHat(opcode);
|
||||||
|
blockCached._blockFunction = runtime.getOpcodeFunction(opcode);
|
||||||
|
blockCached._definedBlockFunction = typeof blockCached._blockFunction !== 'undefined';
|
||||||
|
|
||||||
|
const fieldKeys = Object.keys(fields);
|
||||||
|
|
||||||
|
// Store the current shadow value if there is a shadow value.
|
||||||
|
blockCached._isShadowBlock = fieldKeys.length === 1 && Object.keys(inputs).length === 0;
|
||||||
|
blockCached._shadowValue = blockCached._isShadowBlock && fields[fieldKeys[0]].value;
|
||||||
|
|
||||||
|
// Store a fields copy. If fields is a VARIABLE, LIST, or
|
||||||
|
// BROADCAST_OPTION, store the created values so fields assignment to
|
||||||
|
// argValues does not iterate over fields.
|
||||||
|
blockCached._fields = Object.assign({}, blockCached.fields);
|
||||||
|
blockCached._fieldKind = fieldKeys.length > 0 ? FieldKind.DYNAMIC : FieldKind.NONE;
|
||||||
|
if (fieldKeys.length === 1 && fieldKeys.includes('VARIABLE')) {
|
||||||
|
blockCached._fieldKind = FieldKind.VARIABLE;
|
||||||
|
blockCached._fieldVariable = {
|
||||||
|
id: fields.VARIABLE.id,
|
||||||
|
name: fields.VARIABLE.value
|
||||||
|
};
|
||||||
|
} else if (fieldKeys.length === 1 && fieldKeys.includes('LIST')) {
|
||||||
|
blockCached._fieldKind = FieldKind.LIST;
|
||||||
|
blockCached._fieldList = {
|
||||||
|
id: fields.LIST.id,
|
||||||
|
name: fields.LIST.value
|
||||||
|
};
|
||||||
|
} else if (fieldKeys.length === 1 && fieldKeys.includes('BROADCAST_OPTION')) {
|
||||||
|
blockCached._fieldKind = FieldKind.BROADCAST_OPTION;
|
||||||
|
blockCached._fieldBroadcastOption = {
|
||||||
|
id: fields.BROADCAST_OPTION.id,
|
||||||
|
name: fields.BROADCAST_OPTION.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store a modified inputs. This assures the keys are its own properties
|
||||||
|
// and that custom_block will not be evaluated.
|
||||||
|
blockCached._inputs = Object.assign({}, blockCached.inputs);
|
||||||
|
delete blockCached._inputs.custom_block;
|
||||||
|
|
||||||
|
blockCached._initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opcode = blockCached.opcode;
|
||||||
|
const fields = blockCached._fields;
|
||||||
|
const inputs = blockCached._inputs;
|
||||||
|
const mutation = blockCached.mutation;
|
||||||
|
const blockFunction = blockCached._blockFunction;
|
||||||
|
const isHat = blockCached._isHat;
|
||||||
|
|
||||||
|
// Hats and single-field shadows are implemented slightly differently
|
||||||
|
// from regular blocks.
|
||||||
|
// For hats: if they have an associated block function, it's treated as a
|
||||||
|
// predicate; if not, execution will proceed as a no-op. For single-field
|
||||||
|
// shadows: If the block has a single field, and no inputs, immediately
|
||||||
|
// return the value of the field.
|
||||||
|
if (!blockCached._definedBlockFunction) {
|
||||||
if (!opcode) {
|
if (!opcode) {
|
||||||
log.warn(`Could not get opcode for block: ${currentBlockId}`);
|
log.warn(`Could not get opcode for block: ${currentBlockId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hats and single-field shadows are implemented slightly differently
|
if (recursiveCall === RECURSIVE && blockCached._isShadowBlock) {
|
||||||
// from regular blocks.
|
// One field and no inputs - treat as arg.
|
||||||
// For hats: if they have an associated block function,
|
thread.pushReportedValue(blockCached._shadowValue);
|
||||||
// it's treated as a predicate; if not, execution will proceed as a no-op.
|
thread.status = Thread.STATUS_RUNNING;
|
||||||
// For single-field shadows: If the block has a single field, and no inputs,
|
} else if (isHat) {
|
||||||
// immediately return the value of the field.
|
|
||||||
if (typeof blockFunction === 'undefined') {
|
|
||||||
if (isHat) {
|
|
||||||
// Skip through the block (hat with no predicate).
|
// Skip through the block (hat with no predicate).
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const keys = Object.keys(fields);
|
|
||||||
if (keys.length === 1 && Object.keys(inputs).length === 0) {
|
|
||||||
// One field and no inputs - treat as arg.
|
|
||||||
handleReport(fields[keys[0]].value, sequencer, thread, currentBlockId, opcode, isHat);
|
|
||||||
} else {
|
} else {
|
||||||
log.warn(`Could not get implementation for opcode: ${opcode}`);
|
log.warn(`Could not get implementation for opcode: ${opcode}`);
|
||||||
}
|
}
|
||||||
|
@ -173,37 +242,55 @@ const execute = function (sequencer, thread) {
|
||||||
// Generate values for arguments (inputs).
|
// Generate values for arguments (inputs).
|
||||||
const argValues = {};
|
const argValues = {};
|
||||||
|
|
||||||
// Add all fields on this block to the argValues.
|
// Add all fields on this block to the argValues. Some known fields may
|
||||||
|
// appear by themselves and can be set to argValues quicker by setting them
|
||||||
|
// explicitly.
|
||||||
|
if (blockCached._fieldKind !== FieldKind.NONE) {
|
||||||
|
switch (blockCached._fieldKind) {
|
||||||
|
case FieldKind.VARIABLE:
|
||||||
|
argValues.VARIABLE = blockCached._fieldVariable;
|
||||||
|
break;
|
||||||
|
case FieldKind.LIST:
|
||||||
|
argValues.LIST = blockCached._fieldList;
|
||||||
|
break;
|
||||||
|
case FieldKind.BROADCAST_OPTION:
|
||||||
|
argValues.BROADCAST_OPTION = blockCached._fieldBroadcastOption;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
for (const fieldName in fields) {
|
for (const fieldName in fields) {
|
||||||
if (!fields.hasOwnProperty(fieldName)) continue;
|
|
||||||
if (fieldName === 'VARIABLE' || fieldName === 'LIST' ||
|
|
||||||
fieldName === 'BROADCAST_OPTION') {
|
|
||||||
argValues[fieldName] = {
|
|
||||||
id: fields[fieldName].id,
|
|
||||||
name: fields[fieldName].value
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
argValues[fieldName] = fields[fieldName].value;
|
argValues[fieldName] = fields[fieldName].value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recursively evaluate input blocks.
|
// Recursively evaluate input blocks.
|
||||||
for (const inputName in inputs) {
|
for (const inputName in inputs) {
|
||||||
if (!inputs.hasOwnProperty(inputName)) continue;
|
|
||||||
// Do not evaluate the internal custom command block within definition
|
|
||||||
if (inputName === 'custom_block') continue;
|
|
||||||
const input = inputs[inputName];
|
const input = inputs[inputName];
|
||||||
const inputBlockId = input.block;
|
const inputBlockId = input.block;
|
||||||
// Is there no value for this input waiting in the stack frame?
|
// Is there no value for this input waiting in the stack frame?
|
||||||
if (inputBlockId !== null && typeof currentStackFrame.reported[inputName] === 'undefined') {
|
if (inputBlockId !== null && currentStackFrame.waitingReporter === null) {
|
||||||
// If there's not, we need to evaluate the block.
|
// If there's not, we need to evaluate the block.
|
||||||
// Push to the stack to evaluate the reporter block.
|
// Push to the stack to evaluate the reporter block.
|
||||||
thread.pushStack(inputBlockId);
|
thread.pushStack(inputBlockId);
|
||||||
// Save name of input for `Thread.pushReportedValue`.
|
// Save name of input for `Thread.pushReportedValue`.
|
||||||
currentStackFrame.waitingReporter = inputName;
|
currentStackFrame.waitingReporter = inputName;
|
||||||
// Actually execute the block.
|
// Actually execute the block.
|
||||||
execute(sequencer, thread);
|
execute(sequencer, thread, RECURSIVE);
|
||||||
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
|
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
|
||||||
|
// Waiting for the block to resolve, store the current argValues
|
||||||
|
// onto a member of the currentStackFrame that can be used once
|
||||||
|
// the nested block resolves to rebuild argValues up to this
|
||||||
|
// point.
|
||||||
|
for (const _inputName in inputs) {
|
||||||
|
// We are waiting on the nested block at inputName so we
|
||||||
|
// don't need to store any more inputs.
|
||||||
|
if (_inputName === inputName) break;
|
||||||
|
if (_inputName === 'BROADCAST_INPUT') {
|
||||||
|
currentStackFrame.reported[_inputName] = argValues.BROADCAST_OPTION.name;
|
||||||
|
} else {
|
||||||
|
currentStackFrame.reported[_inputName] = argValues[_inputName];
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +299,24 @@ const execute = function (sequencer, thread) {
|
||||||
currentStackFrame.waitingReporter = null;
|
currentStackFrame.waitingReporter = null;
|
||||||
thread.popStack();
|
thread.popStack();
|
||||||
}
|
}
|
||||||
const inputValue = currentStackFrame.reported[inputName];
|
|
||||||
|
let inputValue;
|
||||||
|
if (currentStackFrame.waitingReporter === null) {
|
||||||
|
inputValue = currentStackFrame.justReported;
|
||||||
|
currentStackFrame.justReported = null;
|
||||||
|
} else if (currentStackFrame.waitingReporter === inputName) {
|
||||||
|
inputValue = currentStackFrame.justReported;
|
||||||
|
currentStackFrame.waitingReporter = null;
|
||||||
|
currentStackFrame.justReported = null;
|
||||||
|
// We have rebuilt argValues with all the stored values in the
|
||||||
|
// currentStackFrame from the nested block's promise resolving.
|
||||||
|
// Using the reported value from the block we waited on, reset the
|
||||||
|
// storage member of currentStackFrame so the next execute call at
|
||||||
|
// this level can use it in a clean state.
|
||||||
|
currentStackFrame.reported = {};
|
||||||
|
} else if (typeof currentStackFrame.reported[inputName] !== 'undefined') {
|
||||||
|
inputValue = currentStackFrame.reported[inputName];
|
||||||
|
}
|
||||||
if (inputName === 'BROADCAST_INPUT') {
|
if (inputName === 'BROADCAST_INPUT') {
|
||||||
const broadcastInput = inputs[inputName];
|
const broadcastInput = inputs[inputName];
|
||||||
// Check if something is plugged into the broadcast block, or
|
// Check if something is plugged into the broadcast block, or
|
||||||
|
@ -239,16 +343,7 @@ const execute = function (sequencer, thread) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any mutation to args (e.g., for procedures).
|
// Add any mutation to args (e.g., for procedures).
|
||||||
const mutation = blockContainer.getMutation(block);
|
|
||||||
if (mutation !== null) {
|
|
||||||
argValues.mutation = mutation;
|
argValues.mutation = mutation;
|
||||||
}
|
|
||||||
|
|
||||||
// If we've gotten this far, all of the input blocks are evaluated,
|
|
||||||
// and `argValues` is fully populated. So, execute the block primitive.
|
|
||||||
// First, clear `currentStackFrame.reported`, so any subsequent execution
|
|
||||||
// (e.g., on return from a branch) gets fresh inputs.
|
|
||||||
currentStackFrame.reported = {};
|
|
||||||
|
|
||||||
let primitiveReportedValue = null;
|
let primitiveReportedValue = null;
|
||||||
blockUtility.sequencer = sequencer;
|
blockUtility.sequencer = sequencer;
|
||||||
|
@ -271,7 +366,7 @@ const execute = function (sequencer, thread) {
|
||||||
runtime.profiler.records.push(runtime.profiler.STOP, performance.now());
|
runtime.profiler.records.push(runtime.profiler.STOP, performance.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof primitiveReportedValue === 'undefined') {
|
if (recursiveCall !== RECURSIVE && typeof primitiveReportedValue === 'undefined') {
|
||||||
// No value reported - potentially a command block.
|
// No value reported - potentially a command block.
|
||||||
// Edge-activated hats don't request a glow; all commands do.
|
// Edge-activated hats don't request a glow; all commands do.
|
||||||
thread.requestScriptGlowInFrame = true;
|
thread.requestScriptGlowInFrame = true;
|
||||||
|
@ -318,8 +413,14 @@ const execute = function (sequencer, thread) {
|
||||||
thread.popStack();
|
thread.popStack();
|
||||||
});
|
});
|
||||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||||
|
if (recursiveCall === RECURSIVE) {
|
||||||
|
// In recursive calls (where execute calls execute) handleReport
|
||||||
|
// simplifies to just calling thread.pushReportedValue.
|
||||||
|
thread.pushReportedValue(primitiveReportedValue);
|
||||||
|
} else {
|
||||||
handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = execute;
|
module.exports = execute;
|
||||||
|
|
|
@ -947,6 +947,9 @@ class Runtime extends EventEmitter {
|
||||||
thread.target = target;
|
thread.target = target;
|
||||||
thread.stackClick = opts.stackClick;
|
thread.stackClick = opts.stackClick;
|
||||||
thread.updateMonitor = opts.updateMonitor;
|
thread.updateMonitor = opts.updateMonitor;
|
||||||
|
thread.blockContainer = opts.updateMonitor ?
|
||||||
|
this.monitorBlocks :
|
||||||
|
target.blocks;
|
||||||
|
|
||||||
thread.pushStack(id);
|
thread.pushStack(id);
|
||||||
this.threads.push(thread);
|
this.threads.push(thread);
|
||||||
|
@ -976,6 +979,7 @@ class Runtime extends EventEmitter {
|
||||||
newThread.target = thread.target;
|
newThread.target = thread.target;
|
||||||
newThread.stackClick = thread.stackClick;
|
newThread.stackClick = thread.stackClick;
|
||||||
newThread.updateMonitor = thread.updateMonitor;
|
newThread.updateMonitor = thread.updateMonitor;
|
||||||
|
newThread.blockContainer = thread.blockContainer;
|
||||||
newThread.pushStack(thread.topBlock);
|
newThread.pushStack(thread.topBlock);
|
||||||
const i = this.threads.indexOf(thread);
|
const i = this.threads.indexOf(thread);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
|
|
|
@ -196,7 +196,11 @@ class Sequencer {
|
||||||
this.runtime.profiler.records.push(
|
this.runtime.profiler.records.push(
|
||||||
this.runtime.profiler.START, executeProfilerId, null, performance.now());
|
this.runtime.profiler.START, executeProfilerId, null, performance.now());
|
||||||
}
|
}
|
||||||
|
if (thread.target === null) {
|
||||||
|
this.retireThread(thread);
|
||||||
|
} else {
|
||||||
execute(this, thread);
|
execute(this, thread);
|
||||||
|
}
|
||||||
if (this.runtime.profiler !== null) {
|
if (this.runtime.profiler !== null) {
|
||||||
// this.runtime.profiler.stop();
|
// this.runtime.profiler.stop();
|
||||||
this.runtime.profiler.records.push(this.runtime.profiler.STOP, performance.now());
|
this.runtime.profiler.records.push(this.runtime.profiler.STOP, performance.now());
|
||||||
|
|
|
@ -42,6 +42,12 @@ class Thread {
|
||||||
*/
|
*/
|
||||||
this.target = null;
|
this.target = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Blocks this thread will execute.
|
||||||
|
* @type {Blocks}
|
||||||
|
*/
|
||||||
|
this.blockContainer = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the thread requests its script to glow during this frame.
|
* Whether the thread requests its script to glow during this frame.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -124,7 +130,8 @@ class Thread {
|
||||||
this.stackFrames.push({
|
this.stackFrames.push({
|
||||||
isLoop: false, // Whether this level of the stack is a loop.
|
isLoop: false, // Whether this level of the stack is a loop.
|
||||||
warpMode: warpMode, // Whether this level is in warp mode.
|
warpMode: warpMode, // Whether this level is in warp mode.
|
||||||
reported: {}, // Collects reported input values.
|
justReported: null, // Reported value from just executed block.
|
||||||
|
reported: {}, // Persists reported inputs during async block.
|
||||||
waitingReporter: null, // Name of waiting reporter.
|
waitingReporter: null, // Name of waiting reporter.
|
||||||
params: {}, // Procedure parameters.
|
params: {}, // Procedure parameters.
|
||||||
executionContext: {} // A context passed to block implementations.
|
executionContext: {} // A context passed to block implementations.
|
||||||
|
@ -209,9 +216,8 @@ class Thread {
|
||||||
*/
|
*/
|
||||||
pushReportedValue (value) {
|
pushReportedValue (value) {
|
||||||
const parentStackFrame = this.peekParentStackFrame();
|
const parentStackFrame = this.peekParentStackFrame();
|
||||||
if (parentStackFrame) {
|
if (parentStackFrame !== null) {
|
||||||
const waitingReporter = parentStackFrame.waitingReporter;
|
parentStackFrame.justReported = typeof value === 'undefined' ? null : value;
|
||||||
parentStackFrame.reported[waitingReporter] = value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
234
src/io/video.js
234
src/io/video.js
|
@ -1,41 +1,16 @@
|
||||||
const log = require('../util/log');
|
|
||||||
|
|
||||||
class Video {
|
class Video {
|
||||||
constructor (runtime) {
|
constructor (runtime) {
|
||||||
/**
|
|
||||||
* Reference to the owning Runtime.
|
|
||||||
* @type{!Runtime}
|
|
||||||
*/
|
|
||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default value for mirrored frames.
|
* @typedef VideoProvider
|
||||||
* @type boolean
|
* @property {Function} enableVideo - Requests camera access from the user, and upon success,
|
||||||
|
* enables the video feed
|
||||||
|
* @property {Function} disableVideo - Turns off the video feed
|
||||||
|
* @property {Function} getFrame - Return frame data from the video feed in
|
||||||
|
* specified dimensions, format, and mirroring.
|
||||||
*/
|
*/
|
||||||
this.mirror = true;
|
this.provider = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache frames for this many ms.
|
|
||||||
* @type number
|
|
||||||
*/
|
|
||||||
this._frameCacheTimeout = 16;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DOM Video element
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._video = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Usermedia stream track
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._track = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores some canvas/frame data per resolution/mirror states
|
|
||||||
*/
|
|
||||||
this._workspace = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id representing a Scratch Renderer skin the video is rendered to for
|
* Id representing a Scratch Renderer skin the video is rendered to for
|
||||||
|
@ -89,6 +64,15 @@ class Video {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a video provider for this device. A default implementation of
|
||||||
|
* a video provider can be found in scratch-gui/src/lib/video/video-provider
|
||||||
|
* @param {VideoProvider} provider - Video provider to use
|
||||||
|
*/
|
||||||
|
setProvider (provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request video be enabled. Sets up video, creates video skin and enables preview.
|
* Request video be enabled. Sets up video, creates video skin and enables preview.
|
||||||
*
|
*
|
||||||
|
@ -97,39 +81,18 @@ class Video {
|
||||||
* @return {Promise.<Video>} resolves a promise to this IO device when video is ready.
|
* @return {Promise.<Video>} resolves a promise to this IO device when video is ready.
|
||||||
*/
|
*/
|
||||||
enableVideo () {
|
enableVideo () {
|
||||||
this.enabled = true;
|
if (!this.provider) return null;
|
||||||
return this._setupVideo();
|
return this.provider.enableVideo().then(() => this._setupPreview());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable video stream (turn video off)
|
* Disable video stream (turn video off)
|
||||||
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
disableVideo () {
|
disableVideo () {
|
||||||
this.enabled = false;
|
|
||||||
// If we have begun a setup process, call _teardown after it completes
|
|
||||||
if (this._singleSetup) {
|
|
||||||
this._singleSetup
|
|
||||||
.then(this._teardown.bind(this))
|
|
||||||
.catch(err => this.onError(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* async part of disableVideo
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_teardown () {
|
|
||||||
// we might be asked to re-enable before _teardown is called, just ignore it.
|
|
||||||
if (this.enabled === false) {
|
|
||||||
this._disablePreview();
|
this._disablePreview();
|
||||||
this._singleSetup = null;
|
if (!this.provider) return null;
|
||||||
// by clearing refs to video and track, we should lose our hold over the camera
|
this.provider.disableVideo();
|
||||||
this._video = null;
|
|
||||||
if (this._track) {
|
|
||||||
this._track.stop();
|
|
||||||
}
|
|
||||||
this._track = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,60 +114,9 @@ class Video {
|
||||||
format = Video.FORMAT_IMAGE_DATA,
|
format = Video.FORMAT_IMAGE_DATA,
|
||||||
cacheTimeout = this._frameCacheTimeout
|
cacheTimeout = this._frameCacheTimeout
|
||||||
}) {
|
}) {
|
||||||
if (!this.videoReady) {
|
if (this.provider) return this.provider.getFrame({dimensions, mirror, format, cacheTimeout});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const [width, height] = dimensions;
|
|
||||||
const workspace = this._getWorkspace({dimensions, mirror: Boolean(mirror)});
|
|
||||||
const {videoWidth, videoHeight} = this._video;
|
|
||||||
const {canvas, context, lastUpdate, cacheData} = workspace;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// if the canvas hasn't been updated...
|
|
||||||
if (lastUpdate + cacheTimeout < now) {
|
|
||||||
|
|
||||||
if (mirror) {
|
|
||||||
context.scale(-1, 1);
|
|
||||||
context.translate(width * -1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.drawImage(this._video,
|
|
||||||
// source x, y, width, height
|
|
||||||
0, 0, videoWidth, videoHeight,
|
|
||||||
// dest x, y, width, height
|
|
||||||
0, 0, width, height
|
|
||||||
);
|
|
||||||
|
|
||||||
context.resetTransform();
|
|
||||||
workspace.lastUpdate = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// each data type has it's own data cache, but the canvas is the same
|
|
||||||
if (!cacheData[format]) {
|
|
||||||
cacheData[format] = {lastUpdate: 0};
|
|
||||||
}
|
|
||||||
const formatCache = cacheData[format];
|
|
||||||
|
|
||||||
if (formatCache.lastUpdate + cacheTimeout < now) {
|
|
||||||
if (format === Video.FORMAT_IMAGE_DATA) {
|
|
||||||
formatCache.lastData = context.getImageData(0, 0, width, height);
|
|
||||||
} else if (format === Video.FORMAT_CANVAS) {
|
|
||||||
// this will never change
|
|
||||||
formatCache.lastUpdate = Infinity;
|
|
||||||
formatCache.lastData = canvas;
|
|
||||||
} else {
|
|
||||||
log.error(`video io error - unimplemented format ${format}`);
|
|
||||||
// cache the null result forever, don't log about it again..
|
|
||||||
formatCache.lastUpdate = Infinity;
|
|
||||||
formatCache.lastData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rather than set to now, this data is as stale as it's canvas is
|
|
||||||
formatCache.lastUpdate = Math.max(workspace.lastUpdate, formatCache.lastUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatCache.lastData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the preview ghost effect
|
* Set the preview ghost effect
|
||||||
|
@ -217,62 +129,6 @@ class Video {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when an error happens. Default implementation is just to log error.
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
* @param {Error} error An error object from getUserMedia or other source of error.
|
|
||||||
*/
|
|
||||||
onError (error) {
|
|
||||||
log.error('Unhandled video io device error', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a video stream.
|
|
||||||
* Should probably be moved to -render or somewhere similar later
|
|
||||||
* @private
|
|
||||||
* @return {Promise} When video has been received, rejected if video is not received
|
|
||||||
*/
|
|
||||||
_setupVideo () {
|
|
||||||
// We cache the result of this setup so that we can only ever have a single
|
|
||||||
// video/getUserMedia request happen at a time.
|
|
||||||
if (this._singleSetup) {
|
|
||||||
return this._singleSetup;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._singleSetup = navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: false,
|
|
||||||
video: {
|
|
||||||
width: {min: 480, ideal: 640},
|
|
||||||
height: {min: 360, ideal: 480}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(stream => {
|
|
||||||
this._video = document.createElement('video');
|
|
||||||
// Use the new srcObject API, falling back to createObjectURL
|
|
||||||
try {
|
|
||||||
this._video.srcObject = stream;
|
|
||||||
} catch (error) {
|
|
||||||
this._video.src = window.URL.createObjectURL(stream);
|
|
||||||
}
|
|
||||||
// Hint to the stream that it should load. A standard way to do this
|
|
||||||
// is add the video tag to the DOM. Since this extension wants to
|
|
||||||
// hide the video tag and instead render a sample of the stream into
|
|
||||||
// the webgl rendered Scratch canvas, another hint like this one is
|
|
||||||
// needed.
|
|
||||||
this._video.play(); // Needed for Safari/Firefox, Chrome auto-plays.
|
|
||||||
this._track = stream.getTracks()[0];
|
|
||||||
this._setupPreview();
|
|
||||||
return this;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this._singleSetup = null;
|
|
||||||
this.onError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this._singleSetup;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disablePreview () {
|
_disablePreview () {
|
||||||
if (this._skin) {
|
if (this._skin) {
|
||||||
this._skin.clear();
|
this._skin.clear();
|
||||||
|
@ -331,53 +187,9 @@ class Video {
|
||||||
}
|
}
|
||||||
|
|
||||||
get videoReady () {
|
get videoReady () {
|
||||||
if (!this.enabled) {
|
if (this.provider) return this.provider.videoReady;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this._video) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this._track) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const {videoWidth, videoHeight} = this._video;
|
|
||||||
if (typeof videoWidth !== 'number' || typeof videoHeight !== 'number') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (videoWidth === 0 || videoHeight === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get an internal workspace for canvas/context/caches
|
|
||||||
* this uses some document stuff to create a canvas and what not, probably needs abstraction
|
|
||||||
* into the renderer layer?
|
|
||||||
* @private
|
|
||||||
* @return {object} A workspace for canvas/data storage. Internal format not documented intentionally
|
|
||||||
*/
|
|
||||||
_getWorkspace ({dimensions, mirror}) {
|
|
||||||
let workspace = this._workspace.find(space => (
|
|
||||||
space.dimensions.join('-') === dimensions.join('-') &&
|
|
||||||
space.mirror === mirror
|
|
||||||
));
|
|
||||||
if (!workspace) {
|
|
||||||
workspace = {
|
|
||||||
dimensions,
|
|
||||||
mirror,
|
|
||||||
canvas: document.createElement('canvas'),
|
|
||||||
lastUpdate: 0,
|
|
||||||
cacheData: {}
|
|
||||||
};
|
|
||||||
workspace.canvas.width = dimensions[0];
|
|
||||||
workspace.canvas.height = dimensions[1];
|
|
||||||
workspace.context = workspace.canvas.getContext('2d');
|
|
||||||
this._workspace.push(workspace);
|
|
||||||
}
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -188,6 +188,10 @@ class VirtualMachine extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVideoProvider (videoProvider) {
|
||||||
|
this.runtime.ioDevices.video.setProvider(videoProvider);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a Scratch project from a .sb, .sb2, .sb3 or json string.
|
* Load a Scratch project from a .sb, .sb2, .sb3 or json string.
|
||||||
* @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
|
* @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
|
||||||
|
@ -545,7 +549,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
* @param {int} costumeIndex - the index of the costume to be got.
|
* @param {int} costumeIndex - the index of the costume to be got.
|
||||||
* @return {string} the costume's SVG string, or null if it's not an SVG costume.
|
* @return {string} the costume's SVG string, or null if it's not an SVG costume.
|
||||||
*/
|
*/
|
||||||
getCostumeSvg (costumeIndex) {
|
getCostume (costumeIndex) {
|
||||||
const id = this.editingTarget.getCostumes()[costumeIndex].assetId;
|
const id = this.editingTarget.getCostumes()[costumeIndex].assetId;
|
||||||
if (id && this.runtime && this.runtime.storage &&
|
if (id && this.runtime && this.runtime.storage &&
|
||||||
this.runtime.storage.get(id).dataFormat === 'svg') {
|
this.runtime.storage.get(id).dataFormat === 'svg') {
|
||||||
|
|
|
@ -91,6 +91,7 @@ const generateThread = function (runtime) {
|
||||||
rt.blocks.createBlock(generateBlock(next));
|
rt.blocks.createBlock(generateBlock(next));
|
||||||
th.pushStack(next);
|
th.pushStack(next);
|
||||||
th.target = rt;
|
th.target = rt;
|
||||||
|
th.blockContainer = rt.blocks;
|
||||||
|
|
||||||
runtime.threads.push(th);
|
runtime.threads.push(th);
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ test('pushReportedValue', t => {
|
||||||
th.pushStack('arbitraryString');
|
th.pushStack('arbitraryString');
|
||||||
th.pushStack('secondString');
|
th.pushStack('secondString');
|
||||||
th.pushReportedValue('value');
|
th.pushReportedValue('value');
|
||||||
t.strictEquals(th.peekParentStackFrame().reported.null, 'value');
|
t.strictEquals(th.peekParentStackFrame().justReported, 'value');
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue