Spaces:
Running
Running
File size: 7,752 Bytes
6bcb42f 10b9827 6bcb42f 10b9827 6bcb42f 10b9827 6bcb42f 10b9827 6bcb42f 10b9827 6bcb42f 10b9827 6bcb42f 10b9827 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
// TODO: access `BlockType` and `ArgumentType` without reaching into VM
// Should we move these into a new extension support module or something?
import ArgumentType from 'scratch-vm/src/extension-support/argument-type';
import BlockType from 'scratch-vm/src/extension-support/block-type';
/**
* Define a block using extension info which has the ability to dynamically determine (and update) its layout.
* This functionality is used for extension blocks which can change its properties based on different state
* information. For example, the `control_stop` block changes its shape based on which menu item is selected
* and a variable block changes its text to reflect the variable name without using an editable field.
* @param {object} ScratchBlocks - The ScratchBlocks name space.
* @param {object} categoryInfo - Information about this block's extension category, including any menus and icons.
* @param {object} staticBlockInfo - The base block information before any dynamic changes.
* @param {string} extendedOpcode - The opcode for the block (including the extension ID).
*/
// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime
const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({
init: function () {
const blockJson = {
type: extendedOpcode,
inputsInline: true,
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3
};
// There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of
// blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension
// blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly
// extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon.
if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) {
blockJson.extensions = ['scratch_extension'];
}
// initialize the basics of the block, to be overridden & extended later by `domToMutation`
this.jsonInit(blockJson);
// initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom`
this.blockInfoText = '{}';
// we need a block info update (through `domToMutation`) before we have a completely initialized block
this.needsBlockInfoUpdate = true;
},
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('blockInfo', this.blockInfoText);
return container;
},
domToMutation: function (xmlElement) {
const blockInfoText = xmlElement.getAttribute('blockInfo');
if (!blockInfoText) return;
if (!this.needsBlockInfoUpdate) {
throw new Error('Attempted to update block info twice');
}
delete this.needsBlockInfoUpdate;
this.blockInfoText = blockInfoText;
const blockInfo = JSON.parse(blockInfoText);
switch (blockInfo.blockType) {
case BlockType.COMMAND:
case BlockType.CONDITIONAL:
case BlockType.LOOP:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setPreviousStatement(true);
this.setNextStatement(!blockInfo.isTerminal);
break;
case BlockType.REPORTER:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND);
if (!blockInfo.disableMonitor) {
this.setCheckboxInFlyout(true);
}
break;
case BlockType.BOOLEAN:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL);
break;
case BlockType.HAT:
case BlockType.EVENT:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setNextStatement(true);
break;
}
if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) {
// `setColour` handles undefined parameters by adjusting defined colors
this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3);
}
// Layout block arguments
// TODO handle E/C Blocks
const blockText = blockInfo.text;
const args = {};
let argCount = 0;
const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => {
const arg = blockInfo.arguments[argName];
switch (arg.type) {
default: // bruh
case ArgumentType.STRING:
args[argName] = { type: 'input_value', name: argName };
break;
case ArgumentType.BOOLEAN:
args[argName] = { type: 'input_value', name: argName, check: 'Boolean' };
break;
}
if (arg.menu && !categoryInfo.menuInfo[arg.menu].acceptsReporters) {
args[argName].type = 'field_dropdown';
args[argName].options = categoryInfo.menuInfo[arg.menu].items;
args[argName].value = categoryInfo.menuInfo[arg.menu].items[0][1];
}
return `%${++argCount}`;
});
this.interpolate_(scratchBlocksStyleText, Object.values(args));
if (this.isInsertionMarker()) return;
for (const name in args) {
if (args[name].type.startsWith('field_')) continue;
const arg = blockInfo.arguments[name];
const connection = this.getInput(name).connection;
const curBlock = connection.targetConnection?.getParentBlock?.();
if (curBlock && curBlock.type !== 'text' && !curBlock.type.startsWith('math_')) continue;
if (arg.menu) {
const fieldId = `${categoryInfo.id}_menu_${arg.menu}`;
if (curBlock?.type === fieldId) continue;
if (curBlock) curBlock.dispose();
const newBlock = this.workspace.newBlock(fieldId);
if (arg.defaultValue) newBlock.getField(arg.menu).setValue(arg.defaultValue);
newBlock.setShadow(true);
newBlock.initSvg();
newBlock.render();
continue;
}
switch (arg.type) {
case ArgumentType.STRING: {
if (curBlock?.type === 'text') break;
if (curBlock) curBlock.dispose();
const newBlock = this.workspace.newBlock('text');
connection.connect(newBlock.outputConnection);
newBlock.getField('TEXT').setValue(arg.defaultValue ?? '');
newBlock.setShadow(true);
newBlock.initSvg();
newBlock.render();
break;
}
case ArgumentType.NUMBER: {
if (curBlock && !curBlock.type.startsWith('math_')) break;
if (curBlock) curBlock.dispose();
const newBlock = this.workspace.newBlock('math_number');
connection.connect(newBlock.outputConnection);
newBlock.getField('NUM').setValue(arg.defaultValue ?? '');
newBlock.setShadow(true);
newBlock.initSvg();
newBlock.render();
break;
}
case ArgumentType.BOOLEAN: {
if (!curBlock) break;
curBlock.dispose();
break;
}
}
}
}
});
export default defineDynamicBlock; |