Merge branch 'develop' into greenkeeper/tap-11.0.1

This commit is contained in:
Andrew Sliwinski 2018-01-19 08:45:58 -05:00 committed by GitHub
commit a9dd658445
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 492 additions and 119 deletions

View file

@ -59,6 +59,7 @@
"socket.io-client": "2.0.4",
"stats.js": "^0.17.0",
"tap": "^11.0.1",
"text-encoding": "0.6.4",
"tiny-worker": "^2.1.1",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.4.1",

View file

@ -233,21 +233,19 @@ class Scratch3LooksBlocks {
looks_cleargraphiceffects: this.clearEffects,
looks_changesizeby: this.changeSize,
looks_setsizeto: this.setSize,
looks_gotofront: this.goToFront,
looks_gobacklayers: this.goBackLayers,
looks_gotofrontback: this.goToFrontBack,
looks_goforwardbackwardlayers: this.goForwardBackwardLayers,
looks_size: this.getSize,
looks_costumeorder: this.getCostumeIndex,
looks_backdroporder: this.getBackdropIndex,
looks_backdropname: this.getBackdropName
looks_costumenumbername: this.getCostumeNumberName,
looks_backdropnumbername: this.getBackdropNumberName
};
}
getMonitored () {
return {
looks_size: {isSpriteSpecific: true},
looks_costumeorder: {isSpriteSpecific: true},
looks_backdroporder: {},
looks_backdropname: {}
looks_costumenumbername: {isSpriteSpecific: true},
looks_backdropnumbername: {}
};
}
@ -409,32 +407,45 @@ class Scratch3LooksBlocks {
util.target.setSize(size);
}
goToFront (args, util) {
goToFrontBack (args, util) {
if (!util.target.isStage) {
util.target.goToFront();
if (args.FRONT_BACK === 'front') {
util.target.goToFront();
} else {
util.target.goToBack();
}
}
}
goBackLayers (args, util) {
util.target.goBackLayers(args.NUM);
goForwardBackwardLayers (args, util) {
if (!util.target.isStage) {
if (args.FORWARD_BACKWARD === 'forward') {
util.target.goForwardLayers(Cast.toNumber(args.NUM));
} else {
util.target.goBackwardLayers(Cast.toNumber(args.NUM));
}
}
}
getSize (args, util) {
return Math.round(util.target.size);
}
getBackdropIndex () {
const stage = this.runtime.getTargetForStage();
return stage.currentCostume + 1;
}
getBackdropName () {
getBackdropNumberName (args) {
const stage = this.runtime.getTargetForStage();
if (args.NUMBER_NAME === 'number') {
return stage.currentCostume + 1;
}
// Else return name
return stage.sprite.costumes[stage.currentCostume].name;
}
getCostumeIndex (args, util) {
return util.target.currentCostume + 1;
getCostumeNumberName (args, util) {
if (args.NUMBER_NAME === 'number') {
return util.target.currentCostume + 1;
}
// Else return name
return util.target.sprite.costumes[util.target.currentCostume].name;
}
}

View file

@ -64,8 +64,8 @@ class Scratch3MotionBlocks {
let targetX = 0;
let targetY = 0;
if (targetName === '_mouse_') {
targetX = util.ioQuery('mouse', 'getX');
targetY = util.ioQuery('mouse', 'getY');
targetX = util.ioQuery('mouse', 'getScratchX');
targetY = util.ioQuery('mouse', 'getScratchY');
} else if (targetName === '_random_') {
const stageWidth = this.runtime.constructor.STAGE_WIDTH;
const stageHeight = this.runtime.constructor.STAGE_HEIGHT;
@ -106,8 +106,8 @@ class Scratch3MotionBlocks {
let targetX = 0;
let targetY = 0;
if (args.TOWARDS === '_mouse_') {
targetX = util.ioQuery('mouse', 'getX');
targetY = util.ioQuery('mouse', 'getY');
targetX = util.ioQuery('mouse', 'getScratchX');
targetY = util.ioQuery('mouse', 'getScratchY');
} else {
const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
if (!pointTarget) return;
@ -155,7 +155,7 @@ class Scratch3MotionBlocks {
util.yield();
}
}
glideTo (args, util) {
const targetXY = this.getTargetXY(args.TO, util);
if (targetXY) {

View file

@ -40,6 +40,7 @@ class Scratch3SensingBlocks {
sensing_of: this.getAttributeOf,
sensing_mousex: this.getMouseX,
sensing_mousey: this.getMouseY,
sensing_setdragmode: this.setDragMode,
sensing_mousedown: this.getMouseDown,
sensing_keypressed: this.getKeyPressed,
sensing_current: this.current,
@ -55,7 +56,6 @@ class Scratch3SensingBlocks {
sensing_answer: {},
sensing_loudness: {},
sensing_timer: {},
sensing_of: {},
sensing_current: {}
};
}
@ -119,8 +119,8 @@ class Scratch3SensingBlocks {
touchingObject (args, util) {
const requestedObject = args.TOUCHINGOBJECTMENU;
if (requestedObject === '_mouse_') {
const mouseX = util.ioQuery('mouse', 'getX');
const mouseY = util.ioQuery('mouse', 'getY');
const mouseX = util.ioQuery('mouse', 'getClientX');
const mouseY = util.ioQuery('mouse', 'getClientY');
return util.target.isTouchingPoint(mouseX, mouseY);
} else if (requestedObject === '_edge_') {
return util.target.isTouchingEdge();
@ -146,8 +146,8 @@ class Scratch3SensingBlocks {
let targetX = 0;
let targetY = 0;
if (args.DISTANCETOMENU === '_mouse_') {
targetX = util.ioQuery('mouse', 'getX');
targetY = util.ioQuery('mouse', 'getY');
targetX = util.ioQuery('mouse', 'getScratchX');
targetY = util.ioQuery('mouse', 'getScratchY');
} else {
const distTarget = this.runtime.getSpriteTargetByName(
args.DISTANCETOMENU
@ -162,6 +162,10 @@ class Scratch3SensingBlocks {
return Math.sqrt((dx * dx) + (dy * dy));
}
setDragMode (args, util) {
util.target.setDraggable(args.DRAG_MODE === 'draggable');
}
getTimer (args, util) {
return util.ioQuery('clock', 'projectTimer');
}
@ -171,11 +175,11 @@ class Scratch3SensingBlocks {
}
getMouseX (args, util) {
return util.ioQuery('mouse', 'getX');
return util.ioQuery('mouse', 'getScratchX');
}
getMouseY (args, util) {
return util.ioQuery('mouse', 'getY');
return util.ioQuery('mouse', 'getScratchY');
}
getMouseDown (args, util) {

View file

@ -3,6 +3,7 @@ const mutationAdapter = require('./mutation-adapter');
const xmlEscape = require('../util/xml-escape');
const MonitorRecord = require('./monitor-record');
const Clone = require('../util/clone');
const {Map} = require('immutable');
/**
* @fileoverview
@ -382,22 +383,38 @@ class Blocks {
block.fields[args.name].id = args.value;
}
} else {
// Changing the value in a dropdown
block.fields[args.name].value = args.value;
if (!optRuntime){
break;
}
const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block;
if (flyoutBlock.isMonitored) {
optRuntime.requestUpdateMonitor(Map({
id: flyoutBlock.id,
params: this._getBlockParams(flyoutBlock)
}));
}
}
break;
case 'mutation':
block.mutation = mutationAdapter(args.value);
break;
case 'checkbox':
case 'checkbox': {
block.isMonitored = args.value;
if (optRuntime) {
const isSpriteSpecific = optRuntime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
optRuntime.monitorBlockInfo[block.opcode].isSpriteSpecific;
block.targetId = isSpriteSpecific ? optRuntime.getEditingTarget().id : null;
if (!optRuntime) {
break;
}
if (optRuntime && wasMonitored && !block.isMonitored) {
const isSpriteSpecific = optRuntime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
optRuntime.monitorBlockInfo[block.opcode].isSpriteSpecific;
block.targetId = isSpriteSpecific ? optRuntime.getEditingTarget().id : null;
if (wasMonitored && !block.isMonitored) {
optRuntime.requestRemoveMonitor(block.id);
} else if (optRuntime && !wasMonitored && block.isMonitored) {
} else if (!wasMonitored && block.isMonitored) {
optRuntime.requestAddMonitor(MonitorRecord({
// @todo(vm#564) this will collide if multiple sprites use same block
id: block.id,
@ -411,6 +428,7 @@ class Blocks {
}
break;
}
}
this.resetCache();
}

View file

@ -2,6 +2,7 @@ const BlockUtility = require('./block-utility');
const log = require('../util/log');
const Thread = require('./thread');
const {Map} = require('immutable');
const cast = require('../util/cast');
/**
* Single BlockUtility instance reused by execute for every pritimive ran.
@ -211,7 +212,30 @@ const execute = function (sequencer, thread) {
currentStackFrame.waitingReporter = null;
thread.popStack();
}
argValues[inputName] = currentStackFrame.reported[inputName];
const inputValue = currentStackFrame.reported[inputName];
if (inputName === 'BROADCAST_INPUT') {
const broadcastInput = inputs[inputName];
// Check if something is plugged into the broadcast block, or
// if the shadow dropdown menu is being used.
if (broadcastInput.block === broadcastInput.shadow) {
// Shadow dropdown menu is being used.
// Get the appropriate information out of it.
const shadow = blockContainer.getBlock(broadcastInput.shadow);
const broadcastField = shadow.fields.BROADCAST_OPTION;
argValues.BROADCAST_OPTION = {
id: broadcastField.id,
name: broadcastField.value
};
} else {
// Something is plugged into the broadcast input.
// Cast it to a string. We don't need an id here.
argValues.BROADCAST_OPTION = {
name: cast.toString(inputValue)
};
}
} else {
argValues[inputName] = inputValue;
}
}
// Add any mutation to args (e.g., for procedures).

View file

@ -98,12 +98,19 @@ class Target extends EventEmitter {
* if it exists.
* @param {string} id Id of the variable.
* @param {string} name Name of the variable.
* @return {!Variable} Variable object.
* @return {?Variable} Variable object.
*/
lookupBroadcastMsg (id, name) {
const broadcastMsg = this.lookupVariableById(id);
let broadcastMsg;
if (id) {
broadcastMsg = this.lookupVariableById(id);
} else if (name) {
broadcastMsg = this.lookupBroadcastByInputValue(name);
} else {
log.error('Cannot find broadcast message if neither id nor name are provided.');
}
if (broadcastMsg) {
if (broadcastMsg.name !== name) {
if (name && (broadcastMsg.name.toLowerCase() !== name.toLowerCase())) {
log.error(`Found broadcast message with id: ${id}, but` +
`its name, ${broadcastMsg.name} did not match expected name ${name}.`);
}
@ -115,6 +122,23 @@ class Target extends EventEmitter {
}
}
/**
* Look up a broadcast message with the given name and return the variable
* if it exists. Does not create a new broadcast message variable if
* it doesn't exist.
* @param {string} name Name of the variable.
* @return {?Variable} Variable object.
*/
lookupBroadcastByInputValue (name) {
const vars = this.variables;
for (const propName in vars) {
if ((vars[propName].type === Variable.BROADCAST_MESSAGE_TYPE) &&
(vars[propName].name.toLowerCase() === name.toLowerCase())) {
return vars[propName];
}
}
}
/**
* Look up a variable object.
* Search begins for local variables; then look for globals.
@ -141,7 +165,7 @@ class Target extends EventEmitter {
* Search begins for local lists; then look for globals.
* @param {!string} id Id of the list.
* @param {!string} name Name of the list.
* @return {!List} List object.
* @return {!Varible} Variable object representing the found/created list.
*/
lookupOrCreateList (id, name) {
const list = this.lookupVariableById(id);

View file

@ -181,14 +181,18 @@ class Scratch3PenBlocks {
* @param {RenderedTarget} target - the target which has moved.
* @param {number} oldX - the previous X position.
* @param {number} oldY - the previous Y position.
* @param {boolean} isForce - whether the movement was forced.
* @private
*/
_onTargetMoved (target, oldX, oldY) {
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
const penState = this._getPenState(target);
this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y);
this.runtime.requestRedraw();
_onTargetMoved (target, oldX, oldY, isForce) {
// Only move the pen if the movement isn't forced (ie. dragged).
if (!isForce) {
const penSkinId = this._getPenLayerID();
if (penSkinId >= 0) {
const penState = this._getPenState(target);
this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y);
this.runtime.requestRedraw();
}
}
}
@ -446,7 +450,7 @@ class Scratch3PenBlocks {
blockType: BlockType.COMMAND,
text: formatMessage({
id: 'pen.setHue',
default: 'set pen hue to [HUE]',
default: 'set pen color to [HUE]',
description: 'legacy pen blocks - set pen color to number'
}),
arguments: {
@ -462,7 +466,7 @@ class Scratch3PenBlocks {
blockType: BlockType.COMMAND,
text: formatMessage({
id: 'pen.changeHue',
default: 'change pen hue by [HUE]',
default: 'change pen color by [HUE]',
description: 'legacy pen blocks - change pen color'
}),
arguments: {
@ -673,6 +677,8 @@ class Scratch3PenBlocks {
const hueValue = Cast.toNumber(args.HUE);
const colorValue = hueValue / 2;
this._setOrChangeColorParam(ColorParam.COLOR, colorValue, penState, false);
this._legacyUpdatePenColor(penState);
}
/**
@ -686,6 +692,8 @@ class Scratch3PenBlocks {
const hueChange = Cast.toNumber(args.HUE);
const colorChange = hueChange / 2;
this._setOrChangeColorParam(ColorParam.COLOR, colorChange, penState, true);
this._legacyUpdatePenColor(penState);
}
/**
@ -705,25 +713,10 @@ class Scratch3PenBlocks {
newShade = newShade % 200;
if (newShade < 0) newShade += 200;
// Create the new color in RGB using the scratch 2 "shade" model
let rgb = Color.hsvToRgb({h: penState.color * 360 / 100, s: 1, v: 1});
const shade = (newShade > 100) ? 200 - newShade : newShade;
if (shade < 50) {
rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60);
} else {
rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60);
}
// Update the pen state according to new color
const hsv = Color.rgbToHsv(rgb);
penState.color = 100 * hsv.h / 360;
penState.saturation = 100 * hsv.s;
penState.brightness = 100 * hsv.v;
// And store the shade that was used to compute this new color for later use.
penState._shade = newShade;
this._updatePenColor(penState);
this._legacyUpdatePenColor(penState);
}
/**
@ -739,6 +732,29 @@ class Scratch3PenBlocks {
this.setPenShadeToNumber({SHADE: penState._shade + shadeChange}, util);
}
/**
* Update the pen state's color from its hue & shade values, Scratch 2.0 style.
* @param {object} penState - update the HSV & RGB values in this pen state from its hue & shade values.
* @private
*/
_legacyUpdatePenColor (penState) {
// Create the new color in RGB using the scratch 2 "shade" model
let rgb = Color.hsvToRgb({h: penState.color * 360 / 100, s: 1, v: 1});
const shade = (penState._shade > 100) ? 200 - penState._shade : penState._shade;
if (shade < 50) {
rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60);
} else {
rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60);
}
// Update the pen state according to new color
const hsv = Color.rgbToHsv(rgb);
penState.color = 100 * hsv.h / 360;
penState.saturation = 100 * hsv.s;
penState.brightness = 100 * hsv.v;
this._updatePenColor(penState);
}
}
module.exports = Scratch3PenBlocks;

View file

@ -40,10 +40,20 @@ class Mouse {
*/
postData (data) {
if (data.x) {
this._x = data.x - (data.canvasWidth / 2);
this._clientX = data.x;
this._scratchX = MathUtil.clamp(
480 * ((data.x / data.canvasWidth) - 0.5),
-240,
240
);
}
if (data.y) {
this._y = data.y - (data.canvasHeight / 2);
this._clientY = data.y;
this._scratchY = MathUtil.clamp(
-360 * ((data.y / data.canvasHeight) - 0.5),
-180,
180
);
}
if (typeof data.isDown !== 'undefined') {
this._isDown = data.isDown;
@ -54,19 +64,35 @@ class Mouse {
}
/**
* Get the X position of the mouse.
* @return {number} Clamped X position of the mouse cursor.
* Get the X position of the mouse in client coordinates.
* @return {number} Non-clamped X position of the mouse cursor.
*/
getX () {
return MathUtil.clamp(this._x, -240, 240);
getClientX () {
return this._clientX;
}
/**
* Get the Y position of the mouse.
* Get the Y position of the mouse in client coordinates.
* @return {number} Non-clamped Y position of the mouse cursor.
*/
getClientY () {
return this._clientY;
}
/**
* Get the X position of the mouse in scratch coordinates.
* @return {number} Clamped X position of the mouse cursor.
*/
getScratchX () {
return this._scratchX;
}
/**
* Get the Y position of the mouse in scratch coordinates.
* @return {number} Clamped Y position of the mouse cursor.
*/
getY () {
return MathUtil.clamp(-this._y, -180, 180);
getScratchY () {
return this._scratchY;
}
/**

View file

@ -170,14 +170,23 @@ const generateVariableIdGetter = (function () {
const globalBroadcastMsgStateGenerator = (function () {
let broadcastMsgNameMap = {};
const allBroadcastFields = [];
const emptyStringName = uid();
return function (topLevel) {
if (topLevel) broadcastMsgNameMap = {};
return {
broadcastMsgMapUpdater: function (name) {
broadcastMsgMapUpdater: function (name, field) {
name = name.toLowerCase();
if (name === '') {
name = emptyStringName;
}
broadcastMsgNameMap[name] = `broadcastMsgId-${name}`;
allBroadcastFields.push(field);
return broadcastMsgNameMap[name];
},
globalBroadcastMsgs: broadcastMsgNameMap
globalBroadcastMsgs: broadcastMsgNameMap,
allBroadcastFields: allBroadcastFields,
emptyMsgName: emptyStringName
};
};
}());
@ -340,6 +349,31 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) {
// all other targets have finished processing.
if (target.isStage) {
const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs;
const allBroadcastMsgFields = globalBroadcastMsgObj.allBroadcastFields;
const oldEmptyMsgName = globalBroadcastMsgObj.emptyMsgName;
if (allBroadcastMsgs[oldEmptyMsgName]) {
// Find a fresh 'messageN'
let currIndex = 1;
while (allBroadcastMsgs[`message${currIndex}`]) {
currIndex += 1;
}
const newEmptyMsgName = `message${currIndex}`;
// Add the new empty message name to the broadcast message
// name map, and assign it the old id.
// Then, delete the old entry in map.
allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[oldEmptyMsgName];
delete allBroadcastMsgs[oldEmptyMsgName];
// Now update all the broadcast message fields with
// the new empty message name.
for (let i = 0; i < allBroadcastMsgFields.length; i++) {
if (allBroadcastMsgFields[i].value === '') {
allBroadcastMsgFields[i].value = newEmptyMsgName;
}
}
}
// Traverse the broadcast message name map and create
// broadcast messages as variables on the stage (which is this
// target).
for (const msgName in allBroadcastMsgs) {
const msgId = allBroadcastMsgs[msgName];
const newMsg = new Variable(
@ -494,6 +528,11 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
if (shadowObscured) {
fieldValue = '#990000';
}
} else if (expectedArg.inputOp === 'event_broadcast_menu') {
fieldName = 'BROADCAST_OPTION';
if (shadowObscured) {
fieldValue = '';
}
} else if (shadowObscured) {
// Filled drop-down menu.
fieldValue = '';
@ -503,6 +542,23 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
name: fieldName,
value: fieldValue
};
// event_broadcast_menus have some extra properties to add to the
// field and a different value than the rest
if (expectedArg.inputOp === 'event_broadcast_menu') {
if (!shadowObscured) {
// Need to update the broadcast message name map with
// the value of this field.
// Also need to provide the fields[fieldName] object,
// so that we can later update its value property, e.g.
// if sb2 message name is empty string, we will later
// replace this field's value with messageN
// once we can traverse through all the existing message names
// and come up with a fresh messageN.
const broadcastId = addBroadcastMsg(fieldValue, fields[fieldName]);
fields[fieldName].id = broadcastId;
}
fields[fieldName].variableType = expectedArg.variableType;
}
activeBlock.children.push({
id: inputUid,
opcode: expectedArg.inputOp,
@ -529,8 +585,14 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
// Add `id` property to variable fields
activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg);
} else if (expectedArg.fieldName === 'BROADCAST_OPTION') {
// add the name in this field to the broadcast msg name map
const broadcastId = addBroadcastMsg(providedArg);
// Add the name in this field to the broadcast msg name map.
// Also need to provide the fields[fieldName] object,
// so that we can later update its value property, e.g.
// if sb2 message name is empty string, we will later
// replace this field's value with messageN
// once we can traverse through all the existing message names
// and come up with a fresh messageN.
const broadcastId = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]);
activeBlock.fields[expectedArg.fieldName].id = broadcastId;
}
const varType = expectedArg.variableType;
@ -539,6 +601,41 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
}
}
}
// Updates for blocks that have new menus (e.g. in Looks)
switch (oldOpcode) {
case 'comeToFront':
activeBlock.fields.FRONT_BACK = {
name: 'FRONT_BACK',
value: 'front'
};
break;
case 'goBackByLayers:':
activeBlock.fields.FORWARD_BACKWARD = {
name: 'FORWARD_BACKWARD',
value: 'backward'
};
break;
case 'backgroundIndex':
activeBlock.fields.NUMBER_NAME = {
name: 'NUMBER_NAME',
value: 'number'
};
break;
case 'sceneName':
activeBlock.fields.NUMBER_NAME = {
name: 'NUMBER_NAME',
value: 'name'
};
break;
case 'costumeIndex':
activeBlock.fields.NUMBER_NAME = {
name: 'NUMBER_NAME',
value: 'number'
};
break;
}
// Special cases to generate mutations.
if (oldOpcode === 'stopScripts') {
// Mutation for stop block: if the argument is 'other scripts',

View file

@ -345,12 +345,12 @@ const specMap = {
]
},
'comeToFront': {
opcode: 'looks_gotofront',
opcode: 'looks_gotofrontback',
argMap: [
]
},
'goBackByLayers:': {
opcode: 'looks_gobacklayers',
opcode: 'looks_goforwardbackwardlayers',
argMap: [
{
type: 'input',
@ -360,12 +360,12 @@ const specMap = {
]
},
'costumeIndex': {
opcode: 'looks_costumeorder',
opcode: 'looks_costumenumbername',
argMap: [
]
},
'sceneName': {
opcode: 'looks_backdropname',
opcode: 'looks_backdropnumbername',
argMap: [
]
},
@ -390,7 +390,7 @@ const specMap = {
]
},
'backgroundIndex': {
opcode: 'looks_backdroporder',
opcode: 'looks_backdropnumbername',
argMap: [
]
},
@ -665,8 +665,9 @@ const specMap = {
opcode: 'event_broadcast',
argMap: [
{
type: 'field',
fieldName: 'BROADCAST_OPTION',
type: 'input',
inputOp: 'event_broadcast_menu',
inputName: 'BROADCAST_INPUT',
variableType: Variable.BROADCAST_MESSAGE_TYPE
}
]
@ -675,8 +676,9 @@ const specMap = {
opcode: 'event_broadcastandwait',
argMap: [
{
type: 'field',
fieldName: 'BROADCAST_OPTION',
type: 'input',
inputOp: 'event_broadcast_menu',
inputName: 'BROADCAST_INPUT',
variableType: Variable.BROADCAST_MESSAGE_TYPE
}
]
@ -859,9 +861,8 @@ const specMap = {
opcode: 'sensing_keypressed',
argMap: [
{
type: 'input',
inputOp: 'sensing_keyoptions',
inputName: 'KEY_OPTION'
type: 'field',
fieldName: 'KEY_OPTION'
}
]
},
@ -934,9 +935,8 @@ const specMap = {
opcode: 'sensing_of',
argMap: [
{
type: 'input',
inputOp: 'sensing_of_property_menu',
inputName: 'PROPERTY'
type: 'field',
fieldName: 'PROPERTY'
},
{
type: 'input',

View file

@ -203,7 +203,7 @@ class RenderedTarget extends Target {
this.x = x;
this.y = y;
}
this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY);
this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY, force);
this.runtime.requestTargetsUpdate(this);
}
@ -616,8 +616,7 @@ class RenderedTarget extends Target {
// Limits test to this Drawable, so this will return true
// even if the clone is obscured by another Drawable.
const pickResult = this.runtime.renderer.pick(
x + (this.runtime.constructor.STAGE_WIDTH / 2),
-y + (this.runtime.constructor.STAGE_HEIGHT / 2),
x, y,
null, null,
[this.drawableID]
);
@ -699,10 +698,29 @@ class RenderedTarget extends Target {
}
/**
* Move back a number of layers.
* @param {number} nLayers How many layers to go back.
* Move to the back layer.
*/
goBackLayers (nLayers) {
goToBack () {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, -Infinity, false, 1);
}
}
/**
* Move forward a number of layers.
* @param {number} nLayers How many layers to go forward.
*/
goForwardLayers (nLayers) {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, nLayers, true, 1);
}
}
/**
* Move backward a number of layers.
* @param {number} nLayers How many layers to go backward.
*/
goBackwardLayers (nLayers) {
if (this.renderer) {
this.renderer.setDrawableOrder(this.drawableID, -nLayers, true, 1);
}
@ -812,7 +830,6 @@ class RenderedTarget extends Target {
newTarget.effects = JSON.parse(JSON.stringify(this.effects));
newTarget.variables = JSON.parse(JSON.stringify(this.variables));
newTarget.lists = JSON.parse(JSON.stringify(this.lists));
newTarget.initDrawable();
newTarget.updateAllDrawableProperties();
newTarget.goBehindOther(this);
return newTarget;
@ -845,11 +862,10 @@ class RenderedTarget extends Target {
*/
postSpriteInfo (data) {
const force = data.hasOwnProperty('force') ? data.force : null;
if (data.hasOwnProperty('x')) {
this.setXY(data.x, this.y, force);
}
if (data.hasOwnProperty('y')) {
this.setXY(this.x, data.y, force);
const isXChanged = data.hasOwnProperty('x');
const isYChanged = data.hasOwnProperty('y');
if (isXChanged || isYChanged) {
this.setXY(isXChanged ? data.x : this.x, isYChanged ? data.y : this.y, force);
}
if (data.hasOwnProperty('direction')) {
this.setDirection(data.direction);

View file

@ -1,3 +1,4 @@
const TextEncoder = require('text-encoding').TextEncoder;
const EventEmitter = require('events');
const centralDispatch = require('./dispatch/central-dispatch');
@ -8,6 +9,7 @@ const sb2 = require('./serialization/sb2');
const sb3 = require('./serialization/sb3');
const StringUtil = require('./util/string-util');
const formatMessage = require('format-message');
const Variable = require('./engine/variable');
const {loadCostume} = require('./import/load-costume.js');
const {loadSound} = require('./import/load-sound.js');
@ -267,6 +269,8 @@ class VirtualMachine extends EventEmitter {
targets.forEach(target => {
this.runtime.targets.push(target);
(/** @type RenderedTarget */ target).updateAllDrawableProperties();
// Ensure unique sprite name
if (target.isSprite()) this.renameSprite(target.id, target.getName());
});
// Select the first target for editing, e.g., the first sprite.
if (wholeProject && (targets.length > 1)) {
@ -447,7 +451,7 @@ class VirtualMachine extends EventEmitter {
addBackdrop (md5ext, backdropObject) {
return loadCostume(md5ext, backdropObject, this.runtime).then(() => {
const stage = this.runtime.getTargetForStage();
stage.sprite.costumes.push(backdropObject);
stage.addCostume(backdropObject);
stage.setCostume(stage.sprite.costumes.length - 1);
});
}
@ -628,7 +632,7 @@ class VirtualMachine extends EventEmitter {
*/
setEditingTarget (targetId) {
// Has the target id changed? If not, exit.
if (targetId === this.editingTarget.id) {
if (this.editingTarget && targetId === this.editingTarget.id) {
return;
}
const target = this.runtime.getTargetById(targetId);
@ -677,6 +681,35 @@ class VirtualMachine extends EventEmitter {
* of the current editing target's blocks.
*/
emitWorkspaceUpdate () {
// Create a list of broadcast message Ids according to the stage variables
const stageVariables = this.runtime.getTargetForStage().variables;
let messageIds = [];
for (const varId in stageVariables) {
if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) {
messageIds.push(varId);
}
}
// Go through all blocks on all targets, removing referenced
// broadcast ids from the list.
for (let i = 0; i < this.runtime.targets.length; i++) {
const currTarget = this.runtime.targets[i];
const currBlocks = currTarget.blocks._blocks;
for (const blockId in currBlocks) {
if (currBlocks[blockId].fields.BROADCAST_OPTION) {
const id = currBlocks[blockId].fields.BROADCAST_OPTION.id;
const index = messageIds.indexOf(id);
if (index !== -1) {
messageIds = messageIds.slice(0, index)
.concat(messageIds.slice(index + 1));
}
}
}
}
// Anything left in messageIds is not referenced by a block, so delete it.
for (let i = 0; i < messageIds.length; i++) {
const id = messageIds[i];
delete this.runtime.getTargetForStage().variables[id];
}
const variableMap = Object.assign({},
this.runtime.getTargetForStage().variables,
this.editingTarget.variables

52
test/unit/blocks_looks.js Normal file
View file

@ -0,0 +1,52 @@
const test = require('tap').test;
const Looks = require('../../src/blocks/scratch3_looks');
const util = {
target: {
currentCostume: 0, // Internally, current costume is 0 indexed
sprite: {
costumes: [
{name: 'first name'},
{name: 'second name'},
{name: 'third name'}
]
}
}
};
const fakeRuntime = {
getTargetForStage: () => util.target, // Just return the dummy target above.
on: () => {} // Stub out listener methods used in constructor.
};
const blocks = new Looks(fakeRuntime);
test('getCostumeNumberName returns 1-indexed costume number', t => {
util.target.currentCostume = 0; // This is 0-indexed.
const args = {NUMBER_NAME: 'number'};
const number = blocks.getCostumeNumberName(args, util);
t.strictEqual(number, 1);
t.end();
});
test('getCostumeNumberName can return costume name', t => {
util.target.currentCostume = 0; // This is 0-indexed.
const args = {NUMBER_NAME: 'name'};
const number = blocks.getCostumeNumberName(args, util);
t.strictEqual(number, 'first name');
t.end();
});
test('getBackdropNumberName returns 1-indexed costume number', t => {
util.target.currentCostume = 2; // This is 0-indexed.
const args = {NUMBER_NAME: 'number'};
const number = blocks.getBackdropNumberName(args, util);
t.strictEqual(number, 3);
t.end();
});
test('getBackdropNumberName can return costume name', t => {
util.target.currentCostume = 2; // This is 0-indexed.
const args = {NUMBER_NAME: 'name'};
const number = blocks.getBackdropNumberName(args, util);
t.strictEqual(number, 'third name');
t.end();
});

View file

@ -1,6 +1,8 @@
const test = require('tap').test;
const Sensing = require('../../src/blocks/scratch3_sensing');
const Runtime = require('../../src/engine/runtime');
const Sprite = require('../../src/sprites/sprite');
const RenderedTarget = require('../../src/sprites/rendered-target');
test('getPrimitives', t => {
const rt = new Runtime();
@ -65,3 +67,19 @@ test('ask and answer with a visible target', t => {
s.askAndWait({QUESTION: expectedQuestion}, util);
});
test('set drag mode', t => {
const runtime = new Runtime();
runtime.requestTargetsUpdate = () => {}; // noop for testing
const sensing = new Sensing(runtime);
const s = new Sprite();
const rt = new RenderedTarget(s, runtime);
sensing.setDragMode({DRAG_MODE: 'not draggable'}, {target: rt});
t.strictEqual(rt.draggable, false);
sensing.setDragMode({DRAG_MODE: 'draggable'}, {target: rt});
t.strictEqual(rt.draggable, true);
t.end();
});

View file

@ -8,8 +8,10 @@ test('spec', t => {
t.type(m, 'object');
t.type(m.postData, 'function');
t.type(m.getX, 'function');
t.type(m.getY, 'function');
t.type(m.getClientX, 'function');
t.type(m.getClientY, 'function');
t.type(m.getScratchX, 'function');
t.type(m.getScratchY, 'function');
t.type(m.getIsDown, 'function');
t.end();
});
@ -19,14 +21,16 @@ test('mouseUp', t => {
const m = new Mouse(rt);
m.postData({
x: 1,
x: -20,
y: 10,
isDown: false,
canvasWidth: 480,
canvasHeight: 360
});
t.strictEquals(m.getX(), -239);
t.strictEquals(m.getY(), 170);
t.strictEquals(m.getClientX(), -20);
t.strictEquals(m.getClientY(), 10);
t.strictEquals(m.getScratchX(), -240);
t.strictEquals(m.getScratchY(), 170);
t.strictEquals(m.getIsDown(), false);
t.end();
});
@ -37,13 +41,32 @@ test('mouseDown', t => {
m.postData({
x: 10,
y: 100,
y: 400,
isDown: true,
canvasWidth: 480,
canvasHeight: 360
});
t.strictEquals(m.getX(), -230);
t.strictEquals(m.getY(), 80);
t.strictEquals(m.getClientX(), 10);
t.strictEquals(m.getClientY(), 400);
t.strictEquals(m.getScratchX(), -230);
t.strictEquals(m.getScratchY(), -180);
t.strictEquals(m.getIsDown(), true);
t.end();
});
test('at zoomed scale', t => {
const rt = new Runtime();
const m = new Mouse(rt);
m.postData({
x: 240,
y: 540,
canvasWidth: 960,
canvasHeight: 720
});
t.strictEquals(m.getClientX(), 240);
t.strictEquals(m.getClientY(), 540);
t.strictEquals(m.getScratchX(), -120);
t.strictEquals(m.getScratchY(), -90);
t.end();
});

View file

@ -293,8 +293,12 @@ test('layers', t => {
a.renderer = renderer;
a.goToFront();
t.equals(a.renderer.order, 5);
a.goBackLayers(2);
a.goBackwardLayers(2);
t.equals(a.renderer.order, 3);
a.goToBack();
t.equals(a.renderer.order, 1);
a.goForwardLayers(1);
t.equals(a.renderer.order, 2);
o.drawableID = 999;
a.goBehindOther(o);
t.equals(a.renderer.order, 1);

View file

@ -287,12 +287,18 @@ test('emitWorkspaceUpdate', t => {
global: {
toXML: () => 'global'
}
},
blocks: {
toXML: () => 'blocks'
}
}, {
variables: {
unused: {
toXML: () => 'unused'
}
},
blocks: {
toXML: () => 'blocks'
}
}, {
variables: {