Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Max Instances Property to Workspace Options #2130

Merged
merged 11 commits into from Nov 28, 2018

Conversation

BeksOmega
Copy link
Collaborator

@BeksOmega BeksOmega commented Nov 20, 2018

The basics

  • I branched from develop
  • My pull request is against develop
  • My code follows the style guide

The details

Resolves

Issue 1435

Also resolves a bug I could not find an issue for (I assume because no one noticed) where you could not duplicate a block, even if there was enough remaining capacity in the workspace to do so, because the context menu was checking against block.getDecendants() which includes blocks that do not get duplicated.

Proposed Changes

  • Adds a maxInstances map of block types -> max instances number to the workspace options object
  • Adds an isDuplicatable() function to Blockly.Block.
  • Disables the block in the toolbox if there are maxInstances number of that block in the workspace.
  • Disables the Duplicate context menu option if the block is not duplicatable.
  • Disables pasting the block from the clipboard if the block is not duplicatable.

Reason for Changes

Adding this feature gives additional flexibility to event-driven and multi-threaded type Blockly applications. It also suggests creating Blockly games that force you to work more "efficiently" (with a limit number, and types, of blocks).

Test Coverage

I tested the changes by following this proceedure:

  1. Add a maxInstances property to the "controls_if" block with a value of 3.
  2. Create 3 instances of the block, see if the block is disabled in the toolbox flyout. (Pass)
    disabledtoolbox
  3. Create 1 instance of the block. Copy it to the clipboard. Press control-v repeatedly. See if the blocks stop pasting after 3 instances. (Pass)
    pastingworking
  4. Create a "controls_if" block with another "controls_if" block inside it. See if the duplicate option is enabled for the inner if, but disabled for the outer if. (Pass)
    duplicateenabledinner
    duplicatedisabledouter
  5. Remove the maxInstaces property from the "controls_if" block.
  6. Add a maxInstances property to the "logic_boolean" block with a value of 1.
  7. Create a "controls_if" block with another "controls_if" block inside it. Attatch a "logic_boolean" block to the inner if. See if the duplicate option is disabled for both blocks. (Pass)
    duplicatedisabledinnerboolean
    duplicatedisabledouterboolean

This proceedure tests all of the features of this PR:

  • Adds maxInstances property to JsonInit Object - It is being read properly in all steps.
  • Adds an isDuplicatable() function to Blockly.Block. - This function is working correctly in all applicable steps (3, 4, 7)
  • Disables the block in the toolbox if there are maxInstances number of that block in the workspace. - This is working correctly in all applicable steps (1)
  • Disables the Duplicate context menu option if the block is not duplicatable. - This is working correctly in all applicable steps (4, 7)
  • Disables pasting the block from the clipboard if the block is not duplicatable. - This is working correctly in all applicable steps (7)

Tested on:

  • Desktop Chrome

Additional Information

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

@BeksOmega
Copy link
Collaborator Author

I signed it!

@googlebot
Copy link

CLAs look good, thanks!

Copy link
Contributor

@RoboErikG RoboErikG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some high level comments on structure.

core/block.js Outdated Show resolved Hide resolved
core/block.js Outdated Show resolved Hide resolved
core/block.js Outdated Show resolved Hide resolved
…k type to max instances). isDuplicate() changed to correctly handle siblings/branches.
@BeksOmega BeksOmega changed the title Added Max Instances Property to Blocks Added Max Instances Property to Workspace Options Nov 25, 2018
Copy link
Contributor

@RoboErikG RoboErikG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structure looks good. Couple more notes on APIs and runtime. Expect one more review to go over nits and style.

Thanks again for making this change!

core/block.js Outdated Show resolved Hide resolved
core/workspace.js Show resolved Hide resolved
core/block.js Outdated Show resolved Hide resolved
Copy link
Member

@NeilFraser NeilFraser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also create a test block in the 'Basic' category of the playground with the words "limit 3 instances":
tests/playground.html?toolbox=test-blocks
That way this feature may be easily tested.

core/block.js Outdated
Blockly.Block.prototype.isDuplicatable = function() {
var copyableBlocks = this.getDescendants(true);
// Remove all "next statement" blocks because they will not be copied.
if (this.getNextBlock()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache the result of getNextBlock rather than calling it twice.

core/block.js Outdated

var checkedTypes = [];
for (var i = 0, checkBlock; checkBlock = copyableBlocks[i]; i++) {
if (checkedTypes.includes(checkBlock.type)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.prototype.includes isn't ES5; not supported by IE. Use indexOf != -1 instead.

However, checkedTypes really should be a map for many reasons (including the one Erik states below), so this problem goes away. Note that indexOf and includes are both O(n), whereas a map lookup is essentially O(1).

* @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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line length.

*/
Blockly.Workspace.prototype.removeTypedBlock = function(block) {
this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type].indexOf(block), 1);
if (this.typedBlocksDB_[block.type].length === 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'!' is shorter than '=== 0'.

if (this.RTL) {
offset *= -1;
}
blocks.sort(function(a, b) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you pull this function out and make it a static helper function, then it won't need to be redeclared every time getBlocklyByType is called. Better performance.

Suggest:
Blockly.Workspace.prototype.getBlocksByType.sortFunc_ = ...

Copy link
Collaborator Author

@BeksOmega BeksOmega Nov 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! 2 things:

  1. The same sort function is used by Blockly.Workspace.prototype.getTopBlocks, so I figure just declare it a private function of the workspace?
  2. I'm not sure how to pass the offset var into the sort method if I separate it out. My thought was to set a .offset property of the sort function, and then have it use this.offset, but declaring properties of functions seems like a bad idea (not even sure if it would work).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Good idea.
  2. I think a property on the function is also a good idea. It's a seldom used but useful ability in JS.

* @return {!Array.<!Blockly.Block>} The blocks of the given type.
*/
Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) {
if (this.typedBlocksDB_[type]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using slice(0) is faster than [].concat:
https://jsperf.com/cloning-arrays/22

Also, try reversing this to drop the 'else'.

if (!this.typedBlocksDB_[type]) {
  return [];
}
var blocks = this.typedBlocksDB_[type].slice(0);

core/block.js Outdated
* decendents will put this block over the workspace\'s capacity this block is
* not duplicatable. If duplicating this block and decendents will put any
* type over their maxInstances this block is not duplicatable.
* @return {boolean} True if duplicatable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer comments to end with punctuation. (Silliest nit, ever.)

core/block.js Outdated
@@ -668,6 +670,43 @@ Blockly.Block.prototype.setMovable = function(movable) {
this.movable_ = movable;
};

/**
* Get whether is block is duplicatable or not. If duplicating this block and
* decendents will put this block over the workspace\'s capacity this block is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the backslash before the apostrophe?

core/block.js Outdated
/**
* Get whether is block is duplicatable or not. If duplicating this block and
* decendents will put this block over the workspace\'s capacity this block is
* not duplicatable. If duplicating this block and decendents will put any
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean: descendants?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still one more 'decendents' on this line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a second instance of decendents -> descendants.

core/blockly.js Outdated
@@ -98,6 +98,13 @@ Blockly.clipboardXml_ = null;
*/
Blockly.clipboardSource_ = null;

/**
* Copied object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a bit more context here. Something like:
Object copied onto the clipboard.

@NeilFraser
Copy link
Member

Don't let the avalanche of comments discourage you. This is an awesome change.

core/block.js Outdated
/**
* Get whether is block is duplicatable or not. If duplicating this block and
* decendents will put this block over the workspace\'s capacity this block is
* not duplicatable. If duplicating this block and decendents will put any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a second instance of decendents -> descendants.

core/blockly.js Outdated
@@ -273,6 +281,7 @@ Blockly.copy_ = function(toCopy) {
}
Blockly.clipboardXml_ = xml;
Blockly.clipboardSource_ = toCopy.workspace;
Blockly.copiedObject_ = toCopy;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this before: What happens if the block on the clipboard gets deleted? I think you may have to optimistically create the copy and then undo it if it causes a count to go over the limit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about it some more. Instead of putting the block stack to copy here can you put the map of typecounts? That way it'll be quick to check when pasting and you won't need to undo anything.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good solution, but could you give me some tips on implimentation?

Currently the map of typecounts is stored as a local variable inside the isDuplicatable function. It could be moved to a property of the block object, but to get an updated version of it you'd need to call some getCopyableBlocksTypeCounts() function (since isDuplicatable stops once it runs into a type that goes over the remainingCapacityOfType).

So if the above doesn't have any gaps in logic, we have two options:

  1. getCopyableBlocksTypeCounts() & isDuplicatable() share duplicate code.
  2. isDuplicatable() calls getCopyableBlocksTypeCounts() and then iterates over the types (like blockly core would do on paste).

Idk if I should minimize iterations, duplicate code, or if there is a different solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with 2. It'll be easier to maintain and visiting every block once shouldn't be too bad.

The method probably belongs in Blockly.utils.getBlockTypeCounts(block).

Copy link
Member

@NeilFraser NeilFraser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for JS style. Wait for Erik to also approve.

core/block.js Outdated
/**
* Get whether is block is duplicatable or not. If duplicating this block and
* decendents will put this block over the workspace\'s capacity this block is
* not duplicatable. If duplicating this block and decendents will put any
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still one more 'decendents' on this line.

core/block.js Outdated
return false;
}

var copyableBlocksTypeCounts = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.create(null) would be better than {}, since if there's ever a block type named "constructor", or "toString", the results would be unexpected.

@@ -189,6 +194,54 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
return blocks;
};

/** Add a block to the list of blocks keyed by type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First line of a JSDoc should be on the next line (/** should be on its own). Here and method below.

if (this.RTL) {
offset *= -1;
}
blocks.sort(function(a, b) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Good idea.
  2. I think a property on the function is also a good idea. It's a seldom used but useful ability in JS.

.eslintrc Outdated
@@ -1,68 +0,0 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undelete this file (from memory I think you should run git checkout develop -- .eslintrc)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I just noticed, my dad was helping me get set up with a better dev environment last night and must have commited some stuff.

Should I remove the changes to the .gitignore as well? (it added anything beneath idea/ to the ignore list)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, go ahead and revert that as well.

@RoboErikG
Copy link
Contributor

Everything looks good to me. Thanks again for this PR!

@RoboErikG RoboErikG merged commit 1c4ba38 into google:develop Nov 28, 2018
@BeksOmega BeksOmega deleted the feature/issue1435 branch November 29, 2018 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants