Added Max Instances Property to Workspace Options (#2130)

* Added Max Instances property to Blocks

* eslint cleanup

* eslint cleanup 2

* Moved maxInstances property from block to workspace (as a map of block type to max instances). isDuplicate() changed to correctly handle siblings/branches.

* eslint cleanup

* Changed checking types to map. Added hasBlockLimits. Fixed Nits.

* Added limit_instances test block. eslint fixes.

* fixup! Added limit_instances test block. eslint fixes.

* Changed sorting objects to a private static function of the workspace. Fixed nits. Undeleted .eslintrc

* Reverted .gitignore file.

* Added getBlockTypeCounts() to utils. Added isCapacityAvailable() to workspace. Changed clipboard to save typeCountsMap rather than object.
This commit is contained in:
BeksOmega
2018-11-27 16:34:21 -08:00
committed by RoboErikG
parent 6169a6488f
commit 1c4ba38300
10 changed files with 221 additions and 35 deletions

2
.gitignore vendored
View File

@@ -11,4 +11,4 @@ npm-debug.log
tests/compile/main_compressed.js
tests/compile/*compiler*.jar
local_build/*compiler*.jar
local_build/local_blockly_compressed.js
local_build/local_blockly_compressed.js

View File

@@ -164,6 +164,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
}
workspace.addTopBlock(this);
workspace.addTypedBlock(this);
// Call an initialization function, if it exists.
if (typeof this.init == 'function') {
@@ -255,6 +256,7 @@ Blockly.Block.prototype.dispose = function(healStack) {
// Remove this block from the workspace's list of top-most blocks.
if (this.workspace) {
this.workspace.removeTopBlock(this);
this.workspace.removeTypedBlock(this);
// Remove from block database.
delete this.workspace.blockDB_[this.id];
this.workspace = null;
@@ -668,6 +670,21 @@ Blockly.Block.prototype.setMovable = function(movable) {
this.movable_ = movable;
};
/**
* Get whether is block is duplicatable or not. If duplicating this block and
* descendants will put this block over the workspace's capacity this block is
* not duplicatable. If duplicating this block and descendants will put any
* type over their maxInstances this block is not duplicatable.
* @return {boolean} True if duplicatable.
*/
Blockly.Block.prototype.isDuplicatable = function() {
if (!this.workspace.hasBlockLimits()) {
return true;
}
return this.workspace.isCapacityAvailable(
Blockly.utils.getBlockTypeCounts(this, true));
};
/**
* Get whether this block is a shadow block or not.
* @return {boolean} True if a shadow.

View File

@@ -98,6 +98,13 @@ Blockly.clipboardXml_ = null;
*/
Blockly.clipboardSource_ = null;
/**
* Map of types to type counts for the clipboard object and descendants.
* @type {Object}
* @private
*/
Blockly.clipboardTypeCounts_ = null;
/**
* Cached value for whether 3D is supported.
* @type {!boolean}
@@ -227,15 +234,18 @@ Blockly.onKeyDown_ = function(e) {
if (e.keyCode == 86) {
// 'v' for paste.
if (Blockly.clipboardXml_) {
Blockly.Events.setGroup(true);
// Pasting always pastes to the main workspace, even if the copy
// started in a flyout workspace.
var workspace = Blockly.clipboardSource_;
if (workspace.isFlyout) {
workspace = workspace.targetWorkspace;
}
workspace.paste(Blockly.clipboardXml_);
Blockly.Events.setGroup(false);
if (Blockly.clipboardTypeCounts_ &&
workspace.isCapacityAvailable(Blockly.clipboardTypeCounts_)) {
Blockly.Events.setGroup(true);
workspace.paste(Blockly.clipboardXml_);
Blockly.Events.setGroup(false);
}
}
} else if (e.keyCode == 90) {
// 'z' for undo 'Z' is for redo.
@@ -273,6 +283,8 @@ Blockly.copy_ = function(toCopy) {
}
Blockly.clipboardXml_ = xml;
Blockly.clipboardSource_ = toCopy.workspace;
Blockly.clipboardTypeCounts_ = toCopy.isComment ? null :
Blockly.utils.getBlockTypeCounts(toCopy, true);
};
/**

View File

@@ -260,11 +260,7 @@ Blockly.ContextMenu.blockHelpOption = function(block) {
* @package
*/
Blockly.ContextMenu.blockDuplicateOption = function(block) {
var enabled = true;
if (block.getDescendants(false).length >
block.workspace.remainingCapacity()) {
enabled = false;
}
var enabled = block.isDuplicatable();
var duplicateOption = {
text: Blockly.Msg['DUPLICATE_BLOCK'],
enabled: enabled,

View File

@@ -228,6 +228,7 @@ Blockly.Flyout.prototype.createDom = function(tagName) {
Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.targetWorkspace_ = targetWorkspace;
this.workspace_.targetWorkspace = targetWorkspace;
// Add scrollbar.
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
this.horizontalLayout_, false, 'blocklyFlyoutScrollbar');
@@ -729,7 +730,8 @@ Blockly.Flyout.prototype.filterForCapacity_ = function() {
for (var i = 0, block; block = blocks[i]; i++) {
if (this.permanentlyDisabled_.indexOf(block) == -1) {
var allBlocks = block.getDescendants(false);
block.setDisabled(allBlocks.length > remainingCapacity);
block.setDisabled(allBlocks.length > remainingCapacity ||
this.targetWorkspace_.remainingCapacityOfType(block.type) <= 0);
}
}
};

View File

@@ -119,6 +119,7 @@ Blockly.Options = function(options) {
this.disable = hasDisable;
this.readOnly = readOnly;
this.maxBlocks = options['maxBlocks'] || Infinity;
this.maxInstances = options['maxInstances'];
this.pathToMedia = pathToMedia;
this.hasCategories = hasCategories;
this.hasScrollbars = hasScrollbars;

View File

@@ -974,3 +974,32 @@ Blockly.utils.containsNode = function(parent, descendant) {
return !!(parent.compareDocumentPosition(descendant) &
Node.DOCUMENT_POSITION_CONTAINED_BY);
};
/**
* Get a map of all the block's descendants mapping their type to the number of
* children with that type.
* @param {!Blockly.Block} block The block to map.
* @param {boolean=} opt_stripFollowing Optionally ignore all following
* statements (blocks that are not inside a value or statement input
* of the block).
* @returns {!Object} Map of types to type counts for descendants of the bock.
*/
Blockly.utils.getBlockTypeCounts = function(block, opt_stripFollowing) {
var typeCountsMap = Object.create(null);
var descendants = block.getDescendants(true);
if (opt_stripFollowing) {
var nextBlock = block.getNextBlock();
if (nextBlock) {
var index = descendants.indexOf(nextBlock);
descendants.splice(index, descendants.length - index);
}
}
for (var i = 0, checkBlock; checkBlock = descendants[i]; i++) {
if (typeCountsMap[checkBlock.type]) {
typeCountsMap[checkBlock.type]++;
} else {
typeCountsMap[checkBlock.type] = 1;
}
}
return typeCountsMap;
};

View File

@@ -85,6 +85,11 @@ Blockly.Workspace = function(opt_options) {
* @private
*/
this.blockDB_ = Object.create(null);
/**
* @type {!Object}
* @private
*/
this.typedBlocksDB_ = Object.create(null);
/**
* A map from variable type to list of variable names. The lists contain all
@@ -147,6 +152,24 @@ Blockly.Workspace.prototype.dispose = function() {
*/
Blockly.Workspace.SCAN_ANGLE = 3;
/**
* Compare function for sorting objects (blocks, comments, etc) by position;
* top to bottom (with slight LTR or RTL bias).
* @param {!Blockly.Block | !Blockly.WorkspaceComment} a The first object to
* compare.
* @param {!Blockly.Block | !Blockly.WorkspaceComment} b The second object to
* compare.
* @returns {number} The comparison value. This tells Array.sort() how to change
* object a's index.
* @private
*/
Blockly.Workspace.prototype.sortObjects_ = function(a, b) {
var aXY = a.getRelativeToSurfaceXY();
var bXY = b.getRelativeToSurfaceXY();
return (aXY.y + Blockly.Workspace.prototype.sortObjects_.offset * aXY.x) -
(bXY.y + Blockly.Workspace.prototype.sortObjects_.offset * bXY.x);
};
/**
* Add a block to the list of top blocks.
* @param {!Blockly.Block} block Block to add.
@@ -175,16 +198,58 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
// Copy the topBlocks_ list.
var blocks = [].concat(this.topBlocks_);
if (ordered && blocks.length > 1) {
var offset =
this.sortObjects_.offset =
Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
offset *= -1;
this.sortObjects_.offset *= -1;
}
blocks.sort(function(a, b) {
var aXY = a.getRelativeToSurfaceXY();
var bXY = b.getRelativeToSurfaceXY();
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
});
blocks.sort(this.sortObjects_);
}
return blocks;
};
/**
* Add a block to the list of blocks keyed by type.
* @param {!Blockly.Block} block Block to add.
*/
Blockly.Workspace.prototype.addTypedBlock = function(block) {
if (!this.typedBlocksDB_[block.type]) {
this.typedBlocksDB_[block.type] = [];
}
this.typedBlocksDB_[block.type].push(block);
};
/**
* Remove a block from the list of blocks keyed by type.
* @param {!Blockly.Block} block Block to remove.
*/
Blockly.Workspace.prototype.removeTypedBlock = function(block) {
this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type]
.indexOf(block), 1);
if (!this.typedBlocksDB_[block.type].length) {
delete this.typedBlocksDB_[block.type];
}
};
/**
* Finds the blocks with the associated type and returns them. Blocks are
* optionally sorted by position; top to bottom (with slight LTR or RTL bias).
* @param {string} type The type of block to search for.
* @param {boolean} ordered Sort the list if true.
* @return {!Array.<!Blockly.Block>} The blocks of the given type.
*/
Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) {
if (!this.typedBlocksDB_[type]) {
return [];
}
var blocks = this.typedBlocksDB_[type].slice(0);
if (ordered && blocks.length > 1) {
this.sortObjects_.offset =
Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
this.sortObjects_.offset *= -1;
}
blocks.sort(this.sortObjects_);
}
return blocks;
};
@@ -232,16 +297,12 @@ Blockly.Workspace.prototype.getTopComments = function(ordered) {
// Copy the topComments_ list.
var comments = [].concat(this.topComments_);
if (ordered && comments.length > 1) {
var offset =
this.sortObjects_.offset =
Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
offset *= -1;
this.sortObjects_.offset *= -1;
}
comments.sort(function(a, b) {
var aXY = a.getRelativeToSurfaceXY();
var bXY = b.getRelativeToSurfaceXY();
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
});
comments.sort(this.sortObjects_);
}
return comments;
};
@@ -455,6 +516,56 @@ Blockly.Workspace.prototype.remainingCapacity = function() {
return this.options.maxBlocks - this.getAllBlocks().length;
};
/**
* The number of blocks of the given type that may be added to the workspace
* before reaching the maxInstances allowed for that type.
* @param {string} type Type of block to return capacity for.
* @return {number} Number of blocks of type left.
*/
Blockly.Workspace.prototype.remainingCapacityOfType = function(type) {
if (!this.options.maxInstances) {
return Infinity;
}
return (this.options.maxInstances[type] || Infinity) -
this.getBlocksByType(type).length;
};
/**
* Check if there is remaining capacity for blocks of the given counts to be
* created. If the total number of blocks represented by the map is more than
* the total remaining capacity, it returns false. If a type count is more
* than the remaining capacity for that type, it returns false.
* @param {!Object} typeCountsMap A map of types to counts (usually representing
* blocks to be created).
* @returns {boolean} True if there is capacity for the given map,
* false otherwise.
*/
Blockly.Workspace.prototype.isCapacityAvailable = function(typeCountsMap) {
if (!this.hasBlockLimits()) {
return true;
}
var copyableBlocksCount = 0;
for (var type in typeCountsMap) {
if (typeCountsMap[type] > this.remainingCapacityOfType(type)) {
return false;
}
copyableBlocksCount += typeCountsMap[type];
}
if (copyableBlocksCount > this.remainingCapacity()) {
return false;
}
return true;
};
/**
* Checks if the workspace has any limits on the maximum number of blocks,
* or the maximum number of blocks of specific types.
* @returns {boolean} True if it has block limits, false otherwise.
*/
Blockly.Workspace.prototype.hasBlockLimits = function() {
return this.options.maxBlocks != Infinity || !!this.options.maxInstances;
};
/**
* Undo or redo the previous action.
* @param {boolean} redo False if undo, true if redo.

View File

@@ -44,6 +44,22 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"output": null,
"colour": 230
},
{
"type": "limit_instances",
"message0": "limit 3 instances %1 %2",
"args0": [
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "STATEMENT"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 230,
},
{
"type": "example_dropdown_long",
"message0": "long: %1",
@@ -145,17 +161,17 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
"type": "example_angle",
"message0": "angle: %1",
"args0": [
{
"type": "field_angle",
"name": "FIELDNAME",
"angle": "90",
"alt":
{
"type": "field_label",
"text": "NO ANGLE FIELD"
}
}
]
{
"type": "field_angle",
"name": "FIELDNAME",
"angle": "90",
"alt":
{
"type": "field_label",
"text": "NO ANGLE FIELD"
}
}
]
},
{
"type": "example_date",

View File

@@ -103,6 +103,7 @@ function start() {
},
horizontalLayout: side == 'top' || side == 'bottom',
maxBlocks: Infinity,
maxInstances: {'limit_instances': 3},
media: '../media/',
oneBasedIndex: true,
readOnly: false,
@@ -1080,6 +1081,7 @@ h1 {
<block type="empty_block_with_mutator"></block>
<block type="value_to_stack"></block>
<block type="value_to_statement"></block>
<block type="limit_instances"></block>
</category>
<category name="Drag">
<label text="Drag each to the workspace"></label>