From 4c8e28c53dd4211ad7ad4e032e20e3e3b692b793 Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Fri, 6 Sep 2019 13:04:01 -0700 Subject: [PATCH] Dynamic output shapes (#2980) * Round and triangle dynamic output shapes. --- core/renderers/common/drawer.js | 12 ++- core/renderers/measurables/connections.js | 9 +++ core/renderers/zelos/constants.js | 96 +++++++++++++++++----- core/renderers/zelos/drawer.js | 98 ++++++++++++++++++++--- core/renderers/zelos/info.js | 52 ++++++++++++ core/renderers/zelos/measurables/rows.js | 28 +++---- tests/playground.html | 4 + 7 files changed, 251 insertions(+), 48 deletions(-) diff --git a/core/renderers/common/drawer.js b/core/renderers/common/drawer.js index 1f8646ef0..704fb41c0 100644 --- a/core/renderers/common/drawer.js +++ b/core/renderers/common/drawer.js @@ -180,9 +180,13 @@ Blockly.blockRendering.Drawer.prototype.drawValueInput_ = function(row) { var input = row.getLastInput(); this.positionExternalValueConnection_(row); + var pathDown = (typeof input.shape.pathDown == "function") ? + input.shape.pathDown(input.height) : + input.shape.pathDown; + this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('H', input.xPos + input.width) + - input.shape.pathDown + + pathDown + Blockly.utils.svgPaths.lineOnAxis('v', row.height - input.connectionHeight); }; @@ -267,10 +271,14 @@ Blockly.blockRendering.Drawer.prototype.drawLeft_ = function() { if (outputConnection) { var tabBottom = outputConnection.connectionOffsetY + outputConnection.height; + var pathUp = (typeof outputConnection.shape.pathUp == "function") ? + outputConnection.shape.pathUp(outputConnection.height) : + outputConnection.shape.pathUp; + // Draw a line up to the bottom of the tab. this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('V', tabBottom) + - outputConnection.shape.pathUp; + pathUp; } // Close off the path. This draws a vertical line up to the start of the // block's path, which may be either a rounded or a sharp corner. diff --git a/core/renderers/measurables/connections.js b/core/renderers/measurables/connections.js index 082c9400e..e8168bf63 100644 --- a/core/renderers/measurables/connections.js +++ b/core/renderers/measurables/connections.js @@ -71,6 +71,15 @@ Blockly.blockRendering.OutputConnection = function(connectionModel) { goog.inherits(Blockly.blockRendering.OutputConnection, Blockly.blockRendering.Connection); +/** + * Whether or not the connection shape is dynamic. Dynamic shapes get their + * height from the block. + * @return {boolean} True if the connection shape is dynamic. + */ +Blockly.blockRendering.OutputConnection.prototype.isDynamic = function() { + return this.shape.isDynamic; +}; + /** * An object containing information about the space a previous connection takes * up during rendering. diff --git a/core/renderers/zelos/constants.js b/core/renderers/zelos/constants.js index 570b470e8..bda2b08d5 100644 --- a/core/renderers/zelos/constants.js +++ b/core/renderers/zelos/constants.js @@ -38,20 +38,23 @@ goog.require('Blockly.utils.svgPaths'); */ Blockly.zelos.ConstantProvider = function() { Blockly.zelos.ConstantProvider.superClass_.constructor.call(this); + + this.GRID_UNIT = 4; - var GRID_UNIT = 4; + this.CORNER_RADIUS = 1 * this.GRID_UNIT; - this.CORNER_RADIUS = 1 * GRID_UNIT; + this.NOTCH_WIDTH = 9 * this.GRID_UNIT; - this.NOTCH_WIDTH = 9 * GRID_UNIT; + this.NOTCH_HEIGHT = 2 * this.GRID_UNIT; - this.NOTCH_HEIGHT = 2 * GRID_UNIT; + this.NOTCH_OFFSET_LEFT = 3 * this.GRID_UNIT; - this.NOTCH_OFFSET_LEFT = 3 * GRID_UNIT; - - this.MIN_BLOCK_HEIGHT = 12 * GRID_UNIT; + this.MIN_BLOCK_HEIGHT = 12 * this.GRID_UNIT; this.DARK_PATH_OFFSET = 0; + + this.TAB_OFFSET_FROM_TOP = 0; + }; goog.inherits(Blockly.zelos.ConstantProvider, Blockly.blockRendering.ConstantProvider); @@ -61,35 +64,78 @@ goog.inherits(Blockly.zelos.ConstantProvider, */ Blockly.zelos.ConstantProvider.prototype.init = function() { Blockly.zelos.ConstantProvider.superClass_.init.call(this); - this.TRIANGLE = this.makeTriangle(); + this.HEXAGONAL = this.makeHexagonal(); + this.ROUNDED = this.makeRounded(); }; /** * @return {!Object} An object containing sizing and path information about - * a triangle shape for connections. + * a hexagonal shape for connections. * @package */ -Blockly.zelos.ConstantProvider.prototype.makeTriangle = function() { - var width = 20; - var height = 20; +Blockly.zelos.ConstantProvider.prototype.makeHexagonal = function() { // The 'up' and 'down' versions of the paths are the same, but the Y sign // flips. Forward and back are the signs to use to move the cursor in the // direction that the path is being drawn. - function makeMainPath(up) { + function makeMainPath(height, up, right) { + var width = height / 2; var forward = up ? -1 : 1; + var direction = right ? -1 : 1; - return Blockly.utils.svgPaths.lineTo(-width, forward * height / 2) + - Blockly.utils.svgPaths.lineTo(width, forward * height / 2); + return Blockly.utils.svgPaths.lineTo(-1 * direction * width, forward * height / 2) + + Blockly.utils.svgPaths.lineTo(direction * width, forward * height / 2); } - var pathUp = makeMainPath(true); - var pathDown = makeMainPath(false); + return { + width: 0, + height: 0, + isDynamic: true, + pathDown: function(height) { + return makeMainPath(height, false, false); + }, + pathUp: function(height) { + return makeMainPath(height, true, false); + }, + pathRightDown: function(height) { + return makeMainPath(height, false, true); + }, + pathRightUp: function(height) { + return makeMainPath(height, false, true); + }, + }; +}; + +/** + * @return {!Object} An object containing sizing and path information about + * a rounded shape for connections. + * @package + */ +Blockly.zelos.ConstantProvider.prototype.makeRounded = function() { + // The 'up' and 'down' versions of the paths are the same, but the Y sign + // flips. Forward and back are the signs to use to move the cursor in the + // direction that the path is being drawn. + function makeMainPath(height, up, right) { + var edgeWidth = height / 2; + return Blockly.utils.svgPaths.arc('a', '0 0 ' + (up || right ? 1 : 0), edgeWidth, + Blockly.utils.svgPaths.point(0, (up ? -1 : 1) * edgeWidth * 2)); + } return { - width: width, - height: height, - pathDown: pathDown, - pathUp: pathUp + width: 0, + height: 0, + isDynamic: true, + pathDown: function(height) { + return makeMainPath(height, false, false); + }, + pathUp: function(height) { + return makeMainPath(height, true, false); + }, + pathRightDown: function(height) { + return makeMainPath(height, false, true); + }, + pathRightUp: function(height) { + return makeMainPath(height, false, true); + }, }; }; @@ -104,7 +150,13 @@ Blockly.zelos.ConstantProvider.prototype.shapeFor = function( case Blockly.OUTPUT_VALUE: // Includes doesn't work in IE. if (checks && checks.indexOf('Boolean') != -1) { - return this.TRIANGLE; + return this.HEXAGONAL; + } + if (checks && checks.indexOf('Number') != -1) { + return this.ROUNDED; + } + if (checks && checks.indexOf('String') != -1) { + return this.ROUNDED; } return this.PUZZLE_TAB; case Blockly.PREVIOUS_STATEMENT: diff --git a/core/renderers/zelos/drawer.js b/core/renderers/zelos/drawer.js index c4ab78419..e6cb1663c 100644 --- a/core/renderers/zelos/drawer.js +++ b/core/renderers/zelos/drawer.js @@ -47,6 +47,21 @@ Blockly.zelos.Drawer = function(block, info) { goog.inherits(Blockly.zelos.Drawer, Blockly.blockRendering.Drawer); +/** + * @override + */ +Blockly.zelos.Drawer.prototype.drawOutline_ = function() { + if (this.info_.outputConnection && + this.info_.outputConnection.isDynamic()) { + this.drawFlatTop_(); + this.drawRightDynamicConnection_(); + this.drawFlatBottom_(); + this.drawLeftDynamicConnection_(); + } else { + Blockly.zelos.Drawer.superClass_.drawOutline_.call(this); + } +}; + /** * Add steps for the top corner of the block, taking into account * details such as hats and rounded corners. @@ -89,24 +104,27 @@ Blockly.zelos.Drawer.prototype.drawBottom_ = function() { var elems = bottomRow.elements; this.positionNextConnection_(); - this.outlinePath_ += - Blockly.utils.svgPaths.lineOnAxis('v', - bottomRow.height - bottomRow.descenderHeight - - this.constants_.INSIDE_CORNERS.rightHeight); - + var rightCornerYOffset = 0; + var outlinePath = ''; for (var i = elems.length - 1, elem; (elem = elems[i]); i--) { if (Blockly.blockRendering.Types.isNextConnection(elem)) { - this.outlinePath_ += elem.shape.pathRight; + outlinePath += elem.shape.pathRight; } else if (Blockly.blockRendering.Types.isLeftSquareCorner(elem)) { - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('H', bottomRow.xPos); + outlinePath += Blockly.utils.svgPaths.lineOnAxis('H', bottomRow.xPos); } else if (Blockly.blockRendering.Types.isLeftRoundedCorner(elem)) { - this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.bottomLeft; + outlinePath += this.constants_.OUTSIDE_CORNERS.bottomLeft; } else if (Blockly.blockRendering.Types.isRightRoundedCorner(elem)) { - this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.bottomRight; + outlinePath += this.constants_.OUTSIDE_CORNERS.bottomRight; + rightCornerYOffset = this.constants_.INSIDE_CORNERS.rightHeight; } else if (Blockly.blockRendering.Types.isSpacer(elem)) { - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('h', elem.width * -1); + outlinePath += Blockly.utils.svgPaths.lineOnAxis('h', elem.width * -1); } } + + this.outlinePath_ += + Blockly.utils.svgPaths.lineOnAxis('V', + bottomRow.baseline - rightCornerYOffset); + this.outlinePath_ += outlinePath; }; /** @@ -134,3 +152,63 @@ Blockly.zelos.Drawer.prototype.drawRightSideRow_ = function(row) { Blockly.utils.svgPaths.lineOnAxis('V', row.yPos + row.height); } }; + +/** + * Add steps to draw the right side of an output with a dynamic connection. + * @protected + */ +Blockly.zelos.Drawer.prototype.drawRightDynamicConnection_ = function() { + this.outlinePath_ += this.info_.outputConnection.shape.pathRightDown( + this.info_.outputConnection.height); +}; + +/** + * Add steps to draw the left side of an output with a dynamic connection. + * @protected + */ +Blockly.zelos.Drawer.prototype.drawLeftDynamicConnection_ = function() { + this.positionOutputConnection_(); + + this.outlinePath_ += this.info_.outputConnection.shape.pathUp( + this.info_.outputConnection.height); + + // Close off the path. This draws a vertical line up to the start of the + // block's path, which may be either a rounded or a sharp corner. + this.outlinePath_ += 'z'; +}; + +/** + * Add steps to draw a flat top row. + * @protected + */ +Blockly.zelos.Drawer.prototype.drawFlatTop_ = function() { + var topRow = this.info_.topRow; + this.positionPreviousConnection_(); + + this.outlinePath_ += + Blockly.utils.svgPaths.moveBy(topRow.xPos, this.info_.startY); + + this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('h', topRow.width); +}; + +/** + * Add steps to draw a flat bottom row. + * @protected + */ +Blockly.zelos.Drawer.prototype.drawFlatBottom_ = function() { + var bottomRow = this.info_.bottomRow; + this.positionNextConnection_(); + + this.outlinePath_ += + Blockly.utils.svgPaths.lineOnAxis('V', bottomRow.baseline); + + this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('h', -bottomRow.width); +}; + +/** + * @override + */ +Blockly.zelos.Drawer.prototype.drawInlineInput_ = function(input) { + // Don't draw an inline input. + this.positionInlineInputConnection_(input); +}; diff --git a/core/renderers/zelos/info.js b/core/renderers/zelos/info.js index 55c448190..770ac6420 100644 --- a/core/renderers/zelos/info.js +++ b/core/renderers/zelos/info.js @@ -88,6 +88,13 @@ goog.inherits(Blockly.zelos.RenderInfo, Blockly.blockRendering.RenderInfo); * @override */ Blockly.zelos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { + if (!prev || !next) { + // No need for padding at the beginning or end of the row if the + // output shape is dynamic. + if (this.outputConnection && this.outputConnection.isDynamic()) { + return this.constants_.NO_PADDING; + } + } if (!prev) { // Between an editable field and the beginning of the row. if (next && Blockly.blockRendering.Types.isField(next) && next.isEditable) { @@ -315,3 +322,48 @@ Blockly.zelos.RenderInfo.prototype.addAlignmentPadding_ = function(row, row.width += missingSpace; } }; + +/** + * @override + */ +Blockly.zelos.RenderInfo.prototype.finalize_ = function() { + // Performance note: this could be combined with the draw pass, if the time + // that this takes is excessive. But it shouldn't be, because it only + // accesses and sets properties that already exist on the objects. + var yCursor = 0; + for (var i = 0, row; (row = this.rows[i]); i++) { + row.yPos = yCursor; + yCursor += row.height; + } + // Dynamic output connections depend on the height of the block. Adjust the + // height and width of the connection, and then adjust the startX and width of the + // block accordingly. + var outputConnectionWidth = 0; + if (this.outputConnection && !this.outputConnection.height) { + this.outputConnection.height = yCursor; + outputConnectionWidth = yCursor; // Twice the width to account for the right side. + this.outputConnection.width = outputConnectionWidth / 2; + } + this.startX += outputConnectionWidth / 2; + this.width += outputConnectionWidth; + + var widestRowWithConnectedBlocks = 0; + for (var i = 0, row; (row = this.rows[i]); i++) { + row.xPos = this.startX; + + widestRowWithConnectedBlocks = + Math.max(widestRowWithConnectedBlocks, row.widthWithConnectedBlocks); + var xCursor = row.xPos; + for (var j = 0, elem; (elem = row.elements[j]); j++) { + elem.xPos = xCursor; + elem.centerline = this.getElemCenterline_(row, elem); + xCursor += elem.width; + } + } + + this.widthWithChildren = widestRowWithConnectedBlocks + this.startX; + + this.height = yCursor; + this.startY = this.topRow.capline; + this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; +}; diff --git a/core/renderers/zelos/measurables/rows.js b/core/renderers/zelos/measurables/rows.js index 7bd7357a2..dd49823dc 100644 --- a/core/renderers/zelos/measurables/rows.js +++ b/core/renderers/zelos/measurables/rows.js @@ -70,21 +70,21 @@ Blockly.zelos.TopRow.prototype.populate = function(block) { }; /** - * Never render a left square corner. Always round. + * Render a round corner unless the block has an output connection. * @override */ -Blockly.zelos.TopRow.prototype.hasLeftSquareCorner = function(_block) { - return false; +Blockly.zelos.TopRow.prototype.hasLeftSquareCorner = function(block) { + return !!block.outputConnection; }; /** * Returns whether or not the top row has a right square corner. - * @param {!Blockly.BlockSvg} _block The block whose top row this represents. + * @param {!Blockly.BlockSvg} block The block whose top row this represents. * @returns {boolean} Whether or not the top row has a left square corner. */ -Blockly.zelos.TopRow.prototype.hasRightSquareCorner = function(_block) { - // Never render a right square corner. Always round. - return false; +Blockly.zelos.TopRow.prototype.hasRightSquareCorner = function(block) { + // Render a round corner unless the block has an output connection. + return !!block.outputConnection; }; /** @@ -120,21 +120,21 @@ Blockly.zelos.BottomRow.prototype.populate = function(block) { }; /** - * Never render a left square corner. Always round. + * Render a round corner unless the block has an output connection. * @override */ -Blockly.zelos.BottomRow.prototype.hasLeftSquareCorner = function(_block) { - return false; +Blockly.zelos.BottomRow.prototype.hasLeftSquareCorner = function(block) { + return !!block.outputConnection; }; /** * Returns whether or not the bottom row has a right square corner. - * @param {!Blockly.BlockSvg} _block The block whose bottom row this represents. + * @param {!Blockly.BlockSvg} block The block whose bottom row this represents. * @returns {boolean} Whether or not the bottom row has a left square corner. */ -Blockly.zelos.BottomRow.prototype.hasRightSquareCorner = function(_block) { - // Never render a right square corner. Always round. - return false; +Blockly.zelos.BottomRow.prototype.hasRightSquareCorner = function(block) { + // Render a round corner unless the block has an output connection. + return !!block.outputConnection; }; /** diff --git a/tests/playground.html b/tests/playground.html index c8e62cb56..30b23d5b7 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -91,6 +91,7 @@ function start() { match = location.search.match(/side=([^&]+)/); var side = match ? match[1] : 'start'; document.forms.options.elements.side.value = side; + var autoimport = !!location.search.match(/autoimport=([^&]+)/); // Create main workspace. workspace = Blockly.inject('blocklyDiv', { @@ -146,6 +147,9 @@ function start() { logEvents(false); } taChange(); + if (autoimport) { + fromXml(); + } } function addToolboxButtonCallbacks() {