From 91a8f4de16cb330f5bdf46aad6c1f5868ab7f619 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 22 Jul 2019 14:10:14 -0700 Subject: [PATCH 1/2] Pull all the new rendering files into develop --- .../block_render_draw.js | 430 +++++++++++ .../block_render_draw_debug.js | 235 ++++++ .../block_render_draw_highlight.js | 224 ++++++ .../block_render_info.js | 678 ++++++++++++++++++ .../block_rendering_constants.js | 357 +++++++++ .../block_rendering_rewrite/measurables.js | 484 +++++++++++++ 6 files changed, 2408 insertions(+) create mode 100644 core/renderers/block_rendering_rewrite/block_render_draw.js create mode 100644 core/renderers/block_rendering_rewrite/block_render_draw_debug.js create mode 100644 core/renderers/block_rendering_rewrite/block_render_draw_highlight.js create mode 100644 core/renderers/block_rendering_rewrite/block_render_info.js create mode 100644 core/renderers/block_rendering_rewrite/block_rendering_constants.js create mode 100644 core/renderers/block_rendering_rewrite/measurables.js diff --git a/core/renderers/block_rendering_rewrite/block_render_draw.js b/core/renderers/block_rendering_rewrite/block_render_draw.js new file mode 100644 index 000000000..da9c84b60 --- /dev/null +++ b/core/renderers/block_rendering_rewrite/block_render_draw.js @@ -0,0 +1,430 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2019 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +//'use strict'; +goog.provide('Blockly.BlockRendering.Drawer'); + +goog.require('Blockly.BlockRendering.Debug'); +goog.require('Blockly.BlockRendering.RenderInfo'); +goog.require('Blockly.BlockRendering.Highlighter'); +goog.require('BRC'); +/* global BRC */ +goog.require('Blockly.BlockRendering.Measurable'); + +/** + * Render the given block. + * @param {!Blockly.BlockSvg} block The block to render + * @public + */ +Blockly.BlockRendering.render = function(block) { + if (!block.renderingDebugger) { + block.renderingDebugger = new Blockly.BlockRendering.Debug(); + } + new Blockly.BlockRendering.Drawer(block).draw_(); +}; + +/** + * An object that draws a block based on the given rendering information. + * @param {!Blockly.BlockSvg} block The block to render + * @param {!Blockly.BlockRendering.RenderInfo} info An object containing all + * information needed to render this block. + * @private + */ +Blockly.BlockRendering.Drawer = function(block) { + this.block_ = block; + this.topLeft_ = block.getRelativeToSurfaceXY(); + this.info_ = new Blockly.BlockRendering.RenderInfo(block); + this.pathObject_ = new Blockly.BlockSvg.PathObject(); + this.steps_ = this.pathObject_.steps; + this.inlineSteps_ = this.pathObject_.inlineSteps; + this.highlighter_ = + new Blockly.BlockRendering.Highlighter(this.info_, this.pathObject_); +}; + +/** + * Draw the block to the workspace. Here "drawing" means setting SVG path + * elements and moving fields, icons, and connections on the screen. + * + * The pieces of the paths are pushed into arrays of "steps", which are then + * joined with spaces and set directly on the block. This guarantees that + * the steps are separated by spaces for improved readability, but isn't + * required. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.draw_ = function() { + this.drawOutline_(); + this.drawInternals_(); + this.block_.setPaths_(this.pathObject_); + this.block_.renderingDebugger.drawDebug(this.block_, this.info_); + this.recordSizeOnBlock_(); +}; + +/** + * Save sizing information back to the block + * Most of the rendering information can be thrown away at the end of the render. + * Anything that needs to be kept around should be set in this function. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.recordSizeOnBlock_ = function() { + // This is used when the block is reporting its size to anyone else. + // The dark path adds to the size of the block in both X and Y. + this.block_.height = this.info_.height + BRC.DARK_PATH_OFFSET; + this.block_.width = this.info_.widthWithChildren + BRC.DARK_PATH_OFFSET; + // The flyout uses this information. + this.block_.startHat_ = this.info_.topRow.startHat; +}; + +/** + * Create the outline of the block. This is a single continuous path. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawOutline_ = function() { + this.drawTop_(); + for (var r = 1; r < this.info_.rows.length - 1; r++) { + var row = this.info_.rows[r]; + if (row.hasStatement) { + this.drawStatementInput_(row); + } else if (row.hasExternalInput) { + this.drawValueInput_(row); + } else { + this.drawRightSideRow_(row); + } + } + this.drawBottom_(); + this.drawLeft_(); +}; + + +/** + * Add steps for the top corner of the block, taking into account + * details such as hats and rounded corners. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawTop_ = function() { + var topRow = this.info_.topRow; + var elements = topRow.elements; + + this.highlighter_.drawTopCorner(topRow); + this.highlighter_.drawRightSideRow(topRow); + this.positionPreviousConnection_(); + + for (var i = 0, elem; elem = elements[i]; i++) { + if (elem.type === 'square corner') { + this.steps_.push(BRC.START_POINT); + } else if (elem.type === 'round corner') { + this.steps_.push(BRC.TOP_LEFT_CORNER_START, BRC.TOP_LEFT_CORNER); + } else if (elem.type === 'previous connection') { + this.steps_.push(BRC.NOTCH_PATH_LEFT); + } else if (elem.type === 'hat') { + this.steps_.push(BRC.START_HAT_PATH); + } else if (elem.isSpacer()) { + this.steps_.push('h', elem.width); + } + } + this.steps_.push('v', topRow.height); +}; + + +/** + * Add steps for an external value input, rendered as a notch in the side + * of the block. + * @param {!Blockly.BlockRendering.Row} row The row that this input + * belongs to. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawValueInput_ = function(row) { + this.highlighter_.drawValueInput(row); + this.steps_.push('H', row.width); + this.steps_.push(BRC.TAB_PATH_DOWN); + this.steps_.push('v', row.height - BRC.TAB_HEIGHT); + this.positionExternalValueConnection_(row); +}; + + +/** + * Add steps for a statement input. + * @param {!Blockly.BlockRendering.Row} row The row that this input + * belongs to. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawStatementInput_ = function(row) { + this.highlighter_.drawStatementInput(row); + var x = row.statementEdge + BRC.NOTCH_OFFSET_RIGHT; + this.steps_.push('H', x); + this.steps_.push(BRC.INNER_TOP_LEFT_CORNER); + this.steps_.push('v', row.height - 2 * BRC.CORNER_RADIUS); + this.steps_.push(BRC.INNER_BOTTOM_LEFT_CORNER); + + this.positionStatementInputConnection_(row); +}; + +/** + * Add steps for the right side of a row that does not have value or + * statement input connections. + * @param {!Blockly.BlockRendering.Row} row The row to draw the + * side of. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawRightSideRow_ = function(row) { + this.highlighter_.drawRightSideRow(row); + this.steps_.push('H', row.width); + this.steps_.push('v', row.height); +}; + + +/** + * Add steps for the bottom edge of a block, possibly including a notch + * for the next connection + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawBottom_ = function() { + var bottomRow = this.info_.bottomRow; + var elems = bottomRow.elements; + this.highlighter_.drawBottomCorner(bottomRow); + this.positionNextConnection_(); + this.steps_.push('v', bottomRow.height); + for (var i = elems.length - 1; i >= 0; i--) { + var elem = elems[i]; + if (elem.type === 'next connection') { + this.steps_.push(BRC.NOTCH_PATH_RIGHT); + } else if (elem.type === 'square corner') { + this.steps_.push('H 0'); + } else if (elem.type === 'round corner') { + this.steps_.push(BRC.BOTTOM_LEFT_CORNER); + } else if (elem.isSpacer()) { + this.steps_.push('h', elem.width * -1); + } + } +}; + +/** + * Add steps for the left side of the block, which may include an output + * connection + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawLeft_ = function() { + this.highlighter_.drawLeft(); + + this.positionOutputConnection_(); + if (this.info_.hasOutputConnection) { + // Draw a line up to the bottom of the tab. + this.steps_.push('V', BRC.TAB_OFFSET_FROM_TOP + BRC.TAB_HEIGHT); + this.steps_.push(BRC.TAB_PATH_UP); + } + // 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.steps_.push('z'); +}; + +/** + * Draw the internals of the block: inline inputs, fields, and icons. These do + * not depend on the outer path for placement. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawInternals_ = function() { + for (var r = 0; r < this.info_.rows.length; r++) { + var row = this.info_.rows[r]; + if (!(row.isSpacer())) { + for (var e = 0; e < row.elements.length; e++) { + var elem = row.elements[e]; + if (elem.isInlineInput()) { + this.drawInlineInput_(elem); + } else if (elem.isIcon() || elem.isField()) { + this.layoutField_(elem); + } + } + } + } +}; + +/** + * Some fields are terrible and render offset from where they claim to be + * rendered. This function calculates an x offset for fields that need it. + * No one is happy about this. + * @param {!Blockly.Field} field The field to find an offset for. + * @return {number} How far to offset the field in the x direction. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.dealWithJackassFields_ = function(field) { + if (field instanceof Blockly.FieldDropdown + || field instanceof Blockly.FieldTextInput + || field instanceof Blockly.FieldColour + || field instanceof Blockly.FieldCheckbox) { + return 5; + } + return 0; +}; + +/** + * Push a field or icon's new position to its SVG root. + * @param {!Blockly.BlockRendering.Icon|!Blockly.BlockRendering.Field} fieldInfo + * The rendering information for the field or icon. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.layoutField_ = function(fieldInfo) { + if (fieldInfo.type == 'field') { + var svgGroup = fieldInfo.field.getSvgRoot(); + } else if (fieldInfo.type == 'icon') { + var svgGroup = fieldInfo.icon.iconGroup_; + } + + var yPos = fieldInfo.centerline - fieldInfo.height / 2; + var xPos = fieldInfo.xPos; + if (this.info_.RTL) { + xPos = -(xPos + fieldInfo.width); + } + if (fieldInfo.type == 'icon') { + svgGroup.setAttribute('display', 'block'); + svgGroup.setAttribute('transform', 'translate(' + xPos + ',' + yPos + ')'); + fieldInfo.icon.computeIconLocation(); + } else { + xPos += this.dealWithJackassFields_(fieldInfo.field); + + svgGroup.setAttribute('transform', 'translate(' + xPos + ',' + yPos + ')'); + } + + if (this.info_.isInsertionMarker) { + // Fields and icons are invisible on insertion marker. They still have to + // be rendered so that the block can be sized correctly. + svgGroup.setAttribute('display', 'none'); + } +}; + +/** + * Add steps for an inline input. + * @param {Blockly.BlockRendering.RenderableInput} input The information about the + * input to render. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.drawInlineInput_ = function(input) { + this.highlighter_.drawInlineInput(input); + var width = input.width; + var height = input.height; + var yPos = input.centerline - height / 2; + + this.inlineSteps_.push('M', (input.xPos + BRC.TAB_WIDTH) + ',' + yPos); + this.inlineSteps_.push('v ', BRC.TAB_OFFSET_FROM_TOP); + this.inlineSteps_.push(BRC.TAB_PATH_DOWN); + this.inlineSteps_.push('v', height - BRC.TAB_HEIGHT - BRC.TAB_OFFSET_FROM_TOP); + this.inlineSteps_.push('h', width - BRC.TAB_WIDTH); + this.inlineSteps_.push('v', -height); + this.inlineSteps_.push('z'); + + this.positionInlineInputConnection_(input); + +}; + +/** + * Position the connection on an inline value input, taking into account + * RTL and the small gap between the parent block and child block which lets the + * parent block's dark path show through. + * @param {Blockly.BlockRendering.RenderableInput} input The information about + * the input that the connection is on. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionInlineInputConnection_ = function(input) { + var yPos = input.centerline - input.height / 2; + // Move the connection. + if (input.connection) { + var connX = input.xPos + BRC.TAB_WIDTH + BRC.DARK_PATH_OFFSET; + if (this.info_.RTL) { + connX *= -1; + } + input.connection.setOffsetInBlock( + connX, yPos + BRC.TAB_OFFSET_FROM_TOP + BRC.DARK_PATH_OFFSET); + } +}; + +/** + * Position the connection on a statement input, taking into account + * RTL and the small gap between the parent block and child block which lets the + * parent block's dark path show through. + * @param {!Blockly.BlockRendering.Row} row The row that the connection is on. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionStatementInputConnection_ = function(row) { + var input = row.getLastInput(); + if (input.connection) { + var connX = row.statementEdge + BRC.NOTCH_OFFSET_LEFT + BRC.DARK_PATH_OFFSET; + if (this.info_.RTL) { + connX *= -1; + } + input.connection.setOffsetInBlock(connX, row.yPos + BRC.DARK_PATH_OFFSET); + } +}; + +/** + * Position the connection on an external value input, taking into account + * RTL and the small gap between the parent block and child block which lets the + * parent block's dark path show through. + * @param {!Blockly.BlockRendering.Row} row The row that the connection is on. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionExternalValueConnection_ = function(row) { + var input = row.getLastInput(); + if (input.connection) { + var connX = row.width + BRC.DARK_PATH_OFFSET; + if (this.info_.RTL) { + connX *= -1; + } + input.connection.setOffsetInBlock(connX, row.yPos); + } +}; + +/** + * Position the previous connection on a block. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionPreviousConnection_ = function() { + if (this.info_.topRow.hasPreviousConnection) { + var connX = this.info_.RTL ? -BRC.NOTCH_OFFSET_LEFT : BRC.NOTCH_OFFSET_LEFT; + this.info_.topRow.connection.setOffsetInBlock(connX, 0); + } +}; + +/** + * Position the next connection on a block. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionNextConnection_ = function() { + var bottomRow = this.info_.bottomRow; + + if (bottomRow.hasNextConnection) { + var connX = this.info_.RTL ? -BRC.NOTCH_OFFSET_LEFT : BRC.NOTCH_OFFSET_LEFT; + bottomRow.connection.setOffsetInBlock( + connX, this.info_.height + BRC.DARK_PATH_OFFSET); + } +}; + +/** + * Position the output connection on a block. + * @param {!Blockly.BlockRendering.BottomRow} row The bottom row on the block. + * @private + */ +Blockly.BlockRendering.Drawer.prototype.positionOutputConnection_ = function() { + if (this.info_.hasOutputConnection) { + this.block_.outputConnection.setOffsetInBlock(0, BRC.TAB_OFFSET_FROM_TOP); + } +}; diff --git a/core/renderers/block_rendering_rewrite/block_render_draw_debug.js b/core/renderers/block_rendering_rewrite/block_render_draw_debug.js new file mode 100644 index 000000000..dd6a8701c --- /dev/null +++ b/core/renderers/block_rendering_rewrite/block_render_draw_debug.js @@ -0,0 +1,235 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2019 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +//'use strict'; +goog.provide('Blockly.BlockRendering.Debug'); +goog.require('Blockly.BlockRendering.RenderInfo'); +goog.require('Blockly.BlockRendering.Highlighter'); +goog.require('BRC'); +goog.require('Blockly.BlockRendering.Measurable'); + +/** + * An object that renders rectangles and dots for debugging rendering code. + * @package + */ +Blockly.BlockRendering.Debug = function() { + /** + * An array of SVG elements that have been created by this object. + * @type {Array.} + */ + this.debugElements_ = []; + + /** + * The SVG root of the block that is being rendered. Debug elements will + * be attached to this root. + * @type {!SVGElement} + */ + this.svgRoot_ = null; +}; + +/** + * Remove all elements the this object created on the last pass. + * @package + */ +Blockly.BlockRendering.Debug.prototype.clearElems = function() { + for (var i = 0, elem; elem = this.debugElements_[i]; i++) { + Blockly.utils.dom.removeNode(elem); + } + + this.debugElements_ = []; +}; + +/** + * Draw a debug rectangle for a spacer (empty) row. + * @param {!Blockly.BlockRendering.Row} row The row to render + * @param {number} cursorY The y position of the top of the row. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawSpacerRow = function(row, cursorY) { + this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', + { + 'class': 'rowSpacerRect blockRenderDebug', + 'x': 0, + 'y': cursorY, + 'width': row.width, + 'height': row.height, + }, + this.svgRoot_)); +}; + +/** + * Draw a debug rectangle for a horizontal spacer. + * @param {!Blockly.BlockSvg.InRowSpacer} elem The spacer to render + * @param {number} cursorX The x position of the left of the row. + * @param {number} centerY The y position of the center of the row, vertically. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawSpacerElem = function(elem, cursorX, centerY) { + var yPos = centerY - elem.height / 2; + this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', + { + 'class': 'elemSpacerRect blockRenderDebug', + 'x': cursorX, + 'y': yPos, + 'width': elem.width, + 'height': 15, + }, + this.svgRoot_)); +}; + +/** + * Draw a debug rectangle for an in-row element. + * @param {!Blockly.BlockSvg.Measurable} elem The element to render + * @param {number} cursorX The x position of the left of the row. + * @param {number} centerY The y position of the center of the row, vertically. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawRenderedElem = function(elem, cursorX, centerY) { + var yPos = centerY - elem.height / 2; + this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', + { + 'class': 'rowRenderingRect blockRenderDebug', + 'x': cursorX, + 'y': yPos, + 'width': elem.width, + 'height': elem.height , + }, + this.svgRoot_)); + + if (elem.isInput) { + this.drawConnection(elem.connection); + } +}; + +/** + * Draw a circle at the location of the given connection. Inputs and outputs + * share the same colors, as do previous and next. When positioned correctly + * a connected pair will look like a bullseye. + * @param {Blockly.RenderedConnection} conn The connection to circle. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawConnection = function(conn) { + var colour; + var size; + var fill; + if (conn.type == Blockly.INPUT_VALUE) { + size = 4; + colour = 'magenta'; + fill = 'none'; + } else if (conn.type == Blockly.OUTPUT_VALUE) { + size = 2; + colour = 'magenta'; + fill = colour; + } else if (conn.type == Blockly.NEXT_STATEMENT) { + size = 4; + colour = 'goldenrod'; + fill = 'none'; + } else if (conn.type == Blockly.PREVIOUS_STATEMENT) { + size = 2; + colour = 'goldenrod'; + fill = colour; + } + this.debugElements_.push(Blockly.utils.dom.createSvgElement('circle', + { + 'class': 'blockRenderDebug', + 'cx': conn.offsetInBlock_.x, + 'cy': conn.offsetInBlock_.y, + 'r': size, + 'fill': fill, + 'stroke': colour, + }, + this.svgRoot_)); +}; + +/** + * Draw a debug rectangle for a non-empty row. + * @param {!Blockly.BlockSvg.Row} row The non-empty row to render. + * @param {number} cursorY The y position of the top of the row. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawRenderedRow = function(row, cursorY) { + this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', + { + 'class': 'elemRenderingRect blockRenderDebug', + 'x': 0, + 'y': cursorY , + 'width': row.width, + 'height': row.height, + }, + this.svgRoot_)); +}; + +/** + * Draw debug rectangles for a non-empty row and all of its subcomponents. + * @param {!Blockly.BlockSvg.Row} row The non-empty row to render. + * @param {number} cursorY The y position of the top of the row. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawRowWithElements = function(row, cursorY) { + var centerY = cursorY + row.height / 2; + var cursorX = 0; + for (var e = 0; e < row.elements.length; e++) { + var elem = row.elements[e]; + if (elem.isSpacer()) { + this.drawSpacerElem(elem, cursorX, centerY); + } else { + this.drawRenderedElem(elem, cursorX, centerY); + } + cursorX += elem.width; + } + this.drawRenderedRow(row, cursorY); +}; + +/** + * Do all of the work to draw debug information for the whole block. + * @param {!Blockly.BlockSvg} block The block to draw debug information for. + * @param {!Blockly.BlockRendering.RenderInfo} info Rendering information about + * the block to debug. + * @package + */ +Blockly.BlockRendering.Debug.prototype.drawDebug = function(block, info) { + this.clearElems(); + this.svgRoot_ = block.getSvgRoot(); + var cursorY = 0; + for (var r = 0; r < info.rows.length; r++) { + var row = info.rows[r]; + if (row.isSpacer()) { + this.drawSpacerRow(row, cursorY); + } else { + this.drawRowWithElements(row, cursorY); + } + cursorY += row.height; + } + + if (block.previousConnection) { + this.drawConnection(block.previousConnection); + } + if (block.nextConnection) { + this.drawConnection(block.nextConnection); + } + if (block.outputConnection) { + this.drawConnection(block.outputConnection); + } +}; diff --git a/core/renderers/block_rendering_rewrite/block_render_draw_highlight.js b/core/renderers/block_rendering_rewrite/block_render_draw_highlight.js new file mode 100644 index 000000000..9ee5ee23a --- /dev/null +++ b/core/renderers/block_rendering_rewrite/block_render_draw_highlight.js @@ -0,0 +1,224 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2019 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +//'use strict'; +goog.provide('Blockly.BlockRendering.Highlighter'); + +goog.require('Blockly.BlockRendering.RenderInfo'); +goog.require('BRC'); +/* global BRC */ +goog.require('Blockly.BlockRendering.Measurable'); + +/** + * An object that adds highlights to a block based on the given rendering + * information. + * + * Highlighting is interesting because the highlights do not fully enclose the + * block. Instead, they are positioned based on a light source in the top left. + * This means that rendering highlights requires exact information about the + * position of each part of the block. The resulting paths are not continuous + * or closed paths. The highlights for tabs and notches are loosely based on + * tab and notch shapes, but are not exactly the same. + * + * @param {!Blockly.BlockRendering.RenderInfo} info An object containing all + * information needed to render this block. + * @param {!Blockly.BlockSvg.PathObject} pathObject An object that stores all of + * the block's paths before they are propagated to the page. + * @package + */ +Blockly.BlockRendering.Highlighter = function(info, pathObject) { + this.info_ = info; + this.pathObject_ = pathObject; + this.highlightSteps_ = this.pathObject_.highlightSteps; + this.highlightInlineSteps_ = this.pathObject_.highlightInlineSteps; +}; + +Blockly.BlockRendering.Highlighter.prototype.drawTopCorner = function(row) { + for (var i = 0, elem; elem = row.elements[i]; i++) { + if (elem.type === 'square corner') { + this.highlightSteps_.push(BRC.START_POINT_HIGHLIGHT); + } else if (elem.type === 'round corner') { + this.highlightSteps_.push(this.info_.RTL ? + BRC.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL : + BRC.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR); + this.highlightSteps_.push(BRC.TOP_LEFT_CORNER_HIGHLIGHT); + } else if (elem.type === 'previous connection') { + this.highlightSteps_.push(BRC.NOTCH_PATH_LEFT_HIGHLIGHT); + } else if (elem.type === 'hat') { + this.highlightSteps_.push(this.info_.RTL ? + Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL : + Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR); + } else if (elem.isSpacer()) { + this.highlightSteps_.push('h', elem.width - BRC.HIGHLIGHT_OFFSET); + } + } + + this.highlightSteps_.push('H', row.width - BRC.HIGHLIGHT_OFFSET); +}; + +Blockly.BlockRendering.Highlighter.prototype.drawValueInput = function(row) { + //var v = row.height - BRC.TAB_HEIGHT; + + if (this.info_.RTL) { + var aboveTabHeight = BRC.TAB_VERTICAL_OVERLAP - BRC.HIGHLIGHT_OFFSET; + var belowTabHeight = row.height - + (BRC.TAB_HEIGHT - BRC.TAB_VERTICAL_OVERLAP) + + BRC.HIGHLIGHT_OFFSET; + // Edge above tab. + this.highlightSteps_.push('v', aboveTabHeight); + // Highlight around back of tab. + this.highlightSteps_.push(BRC.TAB_PATH_DOWN_HIGHLIGHT_RTL); + // Edge below tab. + this.highlightSteps_.push('v', belowTabHeight); + } else { + // Short highlight glint at bottom of tab. + this.highlightSteps_.push('M', (row.width - 5) + ',' + + (row.yPos + BRC.TAB_HEIGHT - 0.7)); + this.highlightSteps_.push('l', (BRC.TAB_WIDTH * 0.46) + ',-2.1'); + } +}; + +Blockly.BlockRendering.Highlighter.prototype.drawStatementInput = function(row) { + var x = row.statementEdge; + if (this.info_.RTL) { + this.highlightSteps_.push('M', + (x + BRC.DISTANCE_45_OUTSIDE) + + ',' + (row.yPos + BRC.DISTANCE_45_OUTSIDE)); + this.highlightSteps_.push( + BRC.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL); + this.highlightSteps_.push('v', + row.height - 2 * BRC.CORNER_RADIUS); + this.highlightSteps_.push( + BRC.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL); + } else { + this.highlightSteps_.push('M', + (x + BRC.DISTANCE_45_OUTSIDE) + ',' + + (row.yPos + row.height - BRC.DISTANCE_45_OUTSIDE)); + this.highlightSteps_.push( + BRC.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR); + } +}; + +Blockly.BlockRendering.Highlighter.prototype.drawRightSideRow = function(row) { + if (row.followsStatement) { + this.highlightSteps_.push('H', row.width); + } + if (this.info_.RTL) { + this.highlightSteps_.push('H', row.width - BRC.HIGHLIGHT_OFFSET); + this.highlightSteps_.push('v', row.height); + } +}; + +Blockly.BlockRendering.Highlighter.prototype.drawBottomCorner = function(_row) { + var height = this.info_.height; + var elems = this.info_.bottomRow.elements; + + if (this.info_.RTL) { + this.highlightSteps_.push('V', height); + } + + for (var i = elems.length - 1; i >= 0; i--) { + var elem = elems[i]; + if (elem.type === 'square corner') { + if (!this.info_.RTL) { + this.highlightSteps_.push('M', + BRC.HIGHLIGHT_OFFSET + ',' + (height - BRC.HIGHLIGHT_OFFSET)); + } + } else if (elem.type === 'round corner') { + if (!this.info_.RTL) { + this.highlightSteps_.push(BRC.BOTTOM_LEFT_CORNER_HIGHLIGHT_START + + (height - Blockly.BlockSvg.DISTANCE_45_INSIDE) + + BRC.BOTTOM_LEFT_CORNER_HIGHLIGHT_MID + + (height - Blockly.BlockSvg.CORNER_RADIUS)); + } + } + } +}; + +Blockly.BlockRendering.Highlighter.prototype.drawLeft = function() { + if (this.info_.hasOutputConnection) { + if (this.info_.RTL) { + this.highlightSteps_.push(BRC.OUTPUT_CONNECTION_HIGHLIGHT_RTL); + } else { + this.highlightSteps_.push(BRC.OUTPUT_CONNECTION_HIGHLIGHT_LTR); + } + } + + if (!this.info_.RTL) { + if (this.info_.topRow.elements[0].isSquareCorner()) { + this.highlightSteps_.push('V', BRC.HIGHLIGHT_OFFSET); + } else { + this.highlightSteps_.push('V', BRC.CORNER_RADIUS); + } + } +}; + +Blockly.BlockRendering.Highlighter.prototype.drawInlineInput = function(input) { + var width = input.width; + var height = input.height; + var x = input.xPos; + var yPos = input.centerline - height / 2; + var bottomHighlightWidth = width - BRC.TAB_WIDTH; + + if (this.info_.RTL) { + // TODO: Check if this is different when the inline input is populated. + + var aboveTabHeight = + BRC.TAB_OFFSET_FROM_TOP + BRC.TAB_VERTICAL_OVERLAP - BRC.HIGHLIGHT_OFFSET; + + var belowTabHeight = + height - + (BRC.TAB_OFFSET_FROM_TOP + BRC.TAB_HEIGHT - BRC.TAB_VERTICAL_OVERLAP) + + BRC.HIGHLIGHT_OFFSET; + + var startX = x + BRC.TAB_WIDTH - BRC.HIGHLIGHT_OFFSET; + var startY = yPos + BRC.HIGHLIGHT_OFFSET; + + // Highlight right edge, around back of tab, and bottom. + this.highlightInlineSteps_.push('M', startX + ',' + startY); + // Right edge above tab. + this.highlightInlineSteps_.push('v', aboveTabHeight); + // Back of tab. + this.highlightInlineSteps_.push(BRC.TAB_PATH_DOWN_HIGHLIGHT_RTL); + // Right edge below tab. + this.highlightInlineSteps_.push('v', belowTabHeight); + // Bottom (horizontal). + this.highlightInlineSteps_.push('h', bottomHighlightWidth); + } else { + // Highlight right edge, bottom. + this.highlightInlineSteps_.push('M', + (x + width + BRC.HIGHLIGHT_OFFSET) + ',' + + (yPos + BRC.HIGHLIGHT_OFFSET)); + this.highlightInlineSteps_.push('v', height); + this.highlightInlineSteps_.push('h ', -bottomHighlightWidth); + // Short highlight glint at bottom of tab. + // Bad: reference to Blockly.BlockSvg + this.highlightInlineSteps_.push('M', + (x + 2.9) + ',' + (yPos + Blockly.BlockSvg.INLINE_PADDING_Y + + BRC.TAB_HEIGHT - 0.7)); + this.highlightInlineSteps_.push('l', + (BRC.TAB_WIDTH * 0.46) + ',-2.1'); + } +}; diff --git a/core/renderers/block_rendering_rewrite/block_render_info.js b/core/renderers/block_rendering_rewrite/block_render_info.js new file mode 100644 index 000000000..19cd3b609 --- /dev/null +++ b/core/renderers/block_rendering_rewrite/block_render_info.js @@ -0,0 +1,678 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2019 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +//'use strict'; + +goog.provide('Blockly.BlockRendering.RenderInfo'); + +goog.require('BRC'); +/* global BRC */ +goog.require('Blockly.BlockRendering.Measurable'); + +/** + * An object containing all sizing information needed to draw this block. + * + * This measure pass does not propagate changes to the block (although fields + * may choose to rerender when getSize() is called). However, calling it + * repeatedly may be expensive. + * + * @param {!Blockly.BlockSvg} block The block to measure. + * @constructor + * @package + */ +Blockly.BlockRendering.RenderInfo = function(block) { + this.block_ = block; + + /** + * Whether the block has an output connection. + * @type {boolean} + */ + this.hasOutputConnection = !!block.outputConnection; + + /** + * Whether the block should be rendered as a single line, either because it's + * inline or because it has been collapsed. + * @type {boolean} + */ + this.isInline = block.getInputsInline() && !block.isCollapsed(); + + /** + * Whether the block is an insertion marker. Insertion markers are the same + * shape as normal blocks, but don't show fields. + * @type {boolean} + */ + this.isInsertionMarker = block.isInsertionMarker(); + + /** + * True if the block should be rendered right-to-left. + * @type {boolean} + */ + this.RTL = block.RTL; + + /** + * The height of the rendered block, including child blocks. + * @type {number} + */ + this.height = 0; + + /** + * The width of the rendered block, including child blocks. + * @type {number} + */ + this.widthWithChildren = 0; + + /** + * The width of the rendered block, excluding child blocks. This is the right + * edge of the block when rendered LTR. + * @type {number} + */ + this.width = 0; + + /** + * + * @type {number} + */ + this.statementEdge = 0; + + /** + * An array of Row objects containing sizing information. + * @type {Array} + */ + this.rows = []; + + this.topRow = null; + this.bottomRow = null; + + this.measure_(); +}; + +/** + * Populate and return an object containing all sizing information needed to + * draw this block. + * + * This measure pass does not propagate changes to the block (although fields + * may choose to rerender when getSize() is called). However, calling it + * repeatedly may be expensive. + * + * @param {!Blockly.BlockSvg} block The block to measure. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.measure_ = function() { + this.createRows_(); + this.addElemSpacing_(); + this.computeBounds_(); + this.alignRowElements_(); + this.addRowSpacing_(); + this.finalize_(); + + console.log(this); +}; + +/** + * Create rows of Measurable objects representing all renderable parts of the + * block. + * @param {!Blockly.BlockSvg} block The block to create rows from. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.createRows_ = function() { + this.createTopRow_(); + this.rows.push(this.topRow); + + var activeRow = new Blockly.BlockRendering.Row(); + + // Icons always go on the first row, before anything else. + var icons = this.block_.getIcons(); + if (icons.length) { + for (var i = 0; i < icons.length; i++) { + activeRow.elements.push( + new Blockly.BlockRendering.Icon(icons[i])); + } + } + + // Loop across all of the inputs on the block, creating objects for anything + // that needs to be rendered and breaking the block up into visual rows. + for (var i = 0; i < this.block_.inputList.length; i++) { + var input = this.block_.inputList[i]; + if (this.shouldStartNewRow_(input, this.block_.inputList[i - 1])) { + // Finish this row and create a new one. + this.rows.push(activeRow); + activeRow = new Blockly.BlockRendering.Row(); + } + + // All of the fields in an input go on the same row. + for (var f = 0; f < input.fieldRow.length; f++) { + var field = input.fieldRow[f]; + activeRow.elements.push(new Blockly.BlockRendering.Field(field, input)); + } + this.addInput_(input, activeRow); + } + + if (activeRow.elements.length) { + this.rows.push(activeRow); + } + this.createBottomRow_(); + this.rows.push(this.bottomRow); +}; + +/** + * Create the top row and fill the elements list with all non-spacer elements + * created. + */ +Blockly.BlockRendering.RenderInfo.prototype.createTopRow_ = function() { + var hasHat = this.block_.hat ? this.block_.hat === 'cap' : Blockly.BlockSvg.START_HAT; + var hasPrevious = !!this.block_.previousConnection; + var prevBlock = this.block_.getPreviousBlock(); + var squareCorner = !!this.block_.outputConnection || + hasHat || (prevBlock && prevBlock.getNextBlock() == this.block_); + this.topRow = new Blockly.BlockRendering.TopRow(this.block_); + + if (squareCorner) { + this.topRow.elements.push(new Blockly.BlockRendering.SquareCorner()); + } else { + this.topRow.elements.push(new Blockly.BlockRendering.RoundCorner()); + } + + if (hasHat) { + this.topRow.elements.push(new Blockly.BlockRendering.Hat()); + } else if (hasPrevious) { + this.topRow.elements.push(new Blockly.BlockRendering.PreviousConnection()); + } +}; + +/** + * Create the bottom row and fill the elements list with all non-spacer elements + * created. + */ +Blockly.BlockRendering.RenderInfo.prototype.createBottomRow_ = function() { + var squareCorner = !!this.block_.outputConnection || !!this.block_.getNextBlock(); + this.bottomRow = new Blockly.BlockRendering.BottomRow(this.block_); + + if (squareCorner) { + this.bottomRow.elements.push(new Blockly.BlockRendering.SquareCorner()); + } else { + this.bottomRow.elements.push(new Blockly.BlockRendering.RoundCorner()); + } + + if (this.bottomRow.hasNextConnection) { + this.bottomRow.elements.push(new Blockly.BlockRendering.NextConnection()); + } +}; + + +/** + * Add an input element to the active row, if needed, and record the type of the + * input on the row. + * @param {!Blockly.Input} input The input to record information about. + * @param {!Blockly.BlockRendering.Row} activeRow The row that is currently being + * populated. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.addInput_ = function(input, activeRow) { + // Non-dummy inputs have visual representations onscreen. + if (this.isInline && input.type == Blockly.INPUT_VALUE) { + activeRow.elements.push(new Blockly.BlockRendering.InlineInput(input)); + activeRow.hasInlineInput = true; + } else if (input.type == Blockly.NEXT_STATEMENT) { + activeRow.elements.push(new Blockly.BlockRendering.StatementInput(input)); + activeRow.hasStatement = true; + } else if (input.type == Blockly.INPUT_VALUE) { + activeRow.elements.push(new Blockly.BlockRendering.ExternalValueInput(input)); + activeRow.hasExternalInput = true; + } else if (input.type == Blockly.DUMMY_INPUT) { + // Dummy inputs have no visual representation, but the information is still + // important. + activeRow.hasDummyInput = true; + } +}; + +/** + * Decide whether to start a new row between the two Blockly.Inputs. + * @param {!Blockly.Input} input The first input to consider + * @param {Blockly.Input} lastInput The input that follows. + * @return {boolean} True if the next input should be rendered on a new row. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.shouldStartNewRow_ = function(input, lastInput) { + // If this is the first input, just add to the existing row. + // That row is either empty or has some icons in it. + if (!lastInput) { + return false; + } + // A statement input always gets a new row. + if (input.type == Blockly.NEXT_STATEMENT) { + return true; + } + // Value and dummy inputs get new row if inputs are not inlined. + if (input.type == Blockly.INPUT_VALUE || input.type == Blockly.DUMMY_INPUT) { + return !this.isInline; + } + return false; +}; + +/** + * Add horizontal spacing between and around elements within each row. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.addElemSpacing_ = function() { + for (var r = 0; r < this.rows.length; r++) { + var row = this.rows[r]; + var oldElems = row.elements; + row.elements = []; + // No spacing needed before the corner on the top row or the bottom row. + if (row.type != 'top row' && row.type != 'bottom row') { + // There's a spacer before the first element in the row. + row.elements.push(new Blockly.BlockRendering.InRowSpacer( + this.getInRowSpacing_(null, oldElems[0]))); + } + + for (var e = 0; e < oldElems.length; e++) { + row.elements.push(oldElems[e]); + var spacing = this.getInRowSpacing_(oldElems[e], oldElems[e + 1]); + row.elements.push(new Blockly.BlockRendering.InRowSpacer(spacing)); + } + } +}; + +/** + * Calculate the width of a spacer element in a row based on the previous and + * next elements in that row. For instance, extra padding is added between two + * editable fields. + * @param {Blockly.BlockRendering.Measurable} prev The element before the + * spacer. + * @param {Blockly.BlockRendering.Measurable} next The element after the spacer. + * @return {number} The size of the spacing between the two elements. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { + if (!prev) { + // Between an editable field and the beginning of the row. + if (next.isField() && next.isEditable) { + return BRC.MEDIUM_PADDING; + } + // Inline input at the beginning of the row. + if (next.isInput && next.isInlineInput()) { + return BRC.MEDIUM_LARGE_PADDING; + } + if (next.isStatementInput()) { + return BRC.STATEMENT_INPUT_PADDING_LEFT; + } + // Anything else at the beginning of the row. + return BRC.LARGE_PADDING; + } + + // Spacing between a field or icon and the end of the row. + if (!prev.isInput && !next) { + // Between an editable field and the end of the row. + if (prev.isField() && prev.isEditable) { + return BRC.MEDIUM_PADDING; + } + // Padding at the end of an icon-only row to make the block shape clearer. + if (prev.isIcon()) { + return (BRC.LARGE_PADDING * 2) + 1; + } + if (prev.isHat()){ + return BRC.NO_PADDING; + } + // Establish a minimum width for a block with a previous or next connection. + if (prev.isPreviousConnection() || prev.isNextConnection()) { + return BRC.LARGE_PADDING; + } + // Between rounded corner and the end of the row. + if (prev.isRoundedCorner()) { + return BRC.MIN_BLOCK_WIDTH; + } + // Between noneditable fields and icons and the end of the row. + return BRC.LARGE_PADDING; + } + + // Between inputs and the end of the row. + if (prev.isInput && !next) { + if (prev.isExternalInput()) { + return BRC.NO_PADDING; + } else if (prev.isInlineInput()) { + return BRC.LARGE_PADDING; + } else if (prev.isStatementInput()) { + return BRC.NO_PADDING; + } + } + + // Spacing between a field or icon and an input. + if (!prev.isInput && next.isInput) { + // Between an editable field and an input. + if (prev.isEditable) { + if (next.isInlineInput()) { + return BRC.SMALL_PADDING; + } else if (next.isExternalInput()) { + return BRC.SMALL_PADDING; + } + } else { + if (next.isInlineInput()) { + return BRC.MEDIUM_LARGE_PADDING; + } else if (next.isExternalInput()) { + return BRC.MEDIUM_LARGE_PADDING; + } else if (next.isStatementInput()) { + return BRC.LARGE_PADDING; + } + } + return BRC.LARGE_PADDING - 1; + } + + // Spacing between an icon and an icon or field. + if (prev.isIcon() && !next.isInput) { + return BRC.LARGE_PADDING; + } + + // Spacing between an inline input and a field. + if (prev.isInlineInput() && !next.isInput) { + // Editable field after inline input. + if (next.isEditable) { + return BRC.MEDIUM_PADDING; + } else { + // Noneditable field after inline input. + return BRC.LARGE_PADDING; + } + } + + if (prev.isSquareCorner()) { + // Spacing between a hat and a corner + if (next.isHat()) { + return BRC.NO_PADDING; + } + // Spacing between a square corner and a previous or next connection + if (next.isPreviousConnection()) { + return BRC.NOTCH_OFFSET_LEFT; + } else if (next.isNextConnection()) { + // Next connections are shifted slightly to the left (in both LTR and RTL) + // to make the dark path under the previous connection show through. + return BRC.NOTCH_OFFSET_LEFT + (this.RTL ? 0.5 : - 0.5); + } + } + + // Spacing between a rounded corner and a previous or next connection + if (prev.isRoundedCorner()){ + if (next.isPreviousConnection()) { + return BRC.NOTCH_OFFSET_ROUNDED_CORNER_PREV; + } else if (next.isNextConnection()) { + // Next connections are shifted slightly to the left (in both LTR and RTL) + // to make the dark path under the previous connection show through. + return BRC.NOTCH_OFFSET_ROUNDED_CORNER_PREV + (this.RTL ? 0.5 : - 0.5); + } + } + + // Spacing between two fields of the same editability. + if (!prev.isInput && !next.isInput && (prev.isEditable == next.isEditable)) { + return BRC.LARGE_PADDING; + } + + return BRC.MEDIUM_PADDING; +}; + +/** + * Figure out where the right edge of the block and right edge of statement inputs + * should be placed. + * TODO: More cleanup. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.computeBounds_ = function() { + var widestStatementRowFields = 0; + var blockWidth = 0; + var widestRowWithConnectedBlocks = 0; + for (var r = 0; r < this.rows.length; r++) { + var row = this.rows[r]; + row.measure(); + if (!row.hasStatement) { + blockWidth = Math.max(blockWidth, row.width); + } + if (row.hasStatement) { + var statementInput = row.getLastInput(); + var innerWidth = row.width - statementInput.width; + widestStatementRowFields = Math.max(widestStatementRowFields, innerWidth); + } + widestRowWithConnectedBlocks = + Math.max(widestRowWithConnectedBlocks, row.widthWithConnectedBlocks); + } + + + this.statementEdge = widestStatementRowFields; + + if (widestStatementRowFields) { + this.width = + Math.max(blockWidth, + widestStatementRowFields + BRC.NOTCH_WIDTH * 2); + } else { + this.width = blockWidth; + } + + for (var r = 0; r < this.rows.length; r++) { + var row = this.rows[r]; + if (row.hasStatement) { + row.statementEdge = this.statementEdge; + } + } + + this.widthWithChildren = + Math.max(blockWidth, widestRowWithConnectedBlocks); +}; + +/** + * Extra spacing may be necessary to make sure that the right sides of all + * rows line up. This can only be calculated after a first pass to calculate + * the sizes of all rows. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.alignRowElements_ = function() { + for (var r = 0; r < this.rows.length; r++) { + var row = this.rows[r]; + if (!row.hasStatement && !row.hasInlineInput) { + var currentWidth = row.width; + var desiredWidth = this.width; + if (row.type === 'bottom row' && row.hasFixedWidth) { + desiredWidth = BRC.MAX_BOTTOM_WIDTH; + } + var missingSpace = desiredWidth - currentWidth; + if (missingSpace) { + this.addAlignmentPadding_(row, missingSpace); + } + } + } +}; + +/** + * Modify the given row to add the given amount of padding around its fields. + * The exact location of the padding is based on the alignment property of the + * last input in the field. + * @param {Blockly.BlockRendering.Row} row The row to add padding to. + * @param {number} missingSpace How much padding to add. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.addAlignmentPadding_ = function(row, missingSpace) { + var elems = row.elements; + var input = row.getLastInput(); + if (input) { + var firstSpacer = row.getFirstSpacer(); + var lastSpacer = row.getLastSpacer(); + if (row.hasExternalInput) { + // Get the spacer right before the input socket. + lastSpacer = elems[elems.length - 3]; + } + // Decide where the extra padding goes. + if (input.align == Blockly.ALIGN_LEFT) { + // Add padding to the end of the row. + lastSpacer.width += missingSpace; + } else if (input.align == Blockly.ALIGN_CENTRE) { + // Split the padding between the beginning and end of the row. + firstSpacer.width += missingSpace / 2; + lastSpacer.width += missingSpace / 2; + } else if (input.align == Blockly.ALIGN_RIGHT) { + // Add padding at the beginning of the row. + firstSpacer.width += missingSpace; + } + row.width += missingSpace; + // Top and bottom rows are always left aligned. + } else if (row.type === 'top row' || row.type === 'bottom row') { + row.getLastSpacer().width += missingSpace; + row.width += missingSpace; + } +}; + +/** + * Add spacers between rows and set their sizes. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.addRowSpacing_ = function() { + var oldRows = this.rows; + this.rows = []; + + for (var r = 0; r < oldRows.length; r++) { + this.rows.push(oldRows[r]); + if (r !== oldRows.length - 1) { + this.rows.push(this.makeSpacerRow_(oldRows[r], oldRows[r + 1])); + } + } +}; + +/** + * Create a spacer row to go between prev and next, and set its size. + * @param {?Blockly.BlockRendering.Measurable} prev The previous row, or null. + * @param {?Blockly.BlockRendering.Measurable} next The next row, or null. + * @return {!Blockly.BlockSvg.BetweenRowSpacer} The newly created spacer row. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.makeSpacerRow_ = function(prev, next) { + var height = this.getSpacerRowHeight_(prev, next); + var width = this.getSpacerRowWidth_(prev, next); + var spacer = new Blockly.BlockRendering.BetweenRowSpacer(height, width); + if (prev.hasStatement) { + spacer.followsStatement = true; + } + return spacer; +}; + +/** + * Calculate the width of a spacer row. Almost all spacers will be the full + * width of the block, but there are some exceptions (e.g. the small spacer row + * after a statement input) + * @param {Blockly.BlockRendering.Row} prev The row before the spacer. + * @param {Blockly.BlockRendering.Row} next The row after the spacer. + * @return {number} The desired width of the spacer row between these two rows. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.getSpacerRowWidth_ = function(prev, next) { + // The width of the spacer before the bottom row should be the same as the + // bottom row. + if (next.type === 'bottom row' + && next.hasFixedWidth) { + return next.width; + } + return this.width; +}; + +/** + * Calculate the height of a spacer row. + * @param {Blockly.BlockRendering.Row} prev The row before the spacer. + * @param {Blockly.BlockRendering.Row} next The row after the spacer. + * @return {number} The desired height of the spacer row between these two rows. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.getSpacerRowHeight_ = function(prev, next) { + // If we have an empty block add a spacer to increase the height + if (prev.type === 'top row' && next.type === 'bottom row') { + return BRC.EMPTY_BLOCK_SPACER_HEIGHT; + } + // Top and bottom rows act as a spacer so we don't need any extra padding + if (prev.type === 'top row' || next.type === 'bottom row') { + return BRC.NO_PADDING; + } + if (prev.hasExternalInput && next.hasExternalInput) { + return BRC.LARGE_PADDING; + } + if (!prev.hasStatement && next.hasStatement) { + return BRC.BETWEEN_STATEMENT_PADDING_Y; + } + if (prev.hasStatement && next.hasStatement) { + return BRC.LARGE_PADDING; + } + if (next.hasDummyInput) { + return BRC.LARGE_PADDING; + } + return BRC.MEDIUM_PADDING; +}; + +/** + * Calculate the centerline of an element in a rendered row. + * @param {Blockly.BlockRendering.Row} row The row containing the element. + * @param {Blockly.BlockRendering.Measurable} elem The element to place. + * @return {number} The desired centerline of the given element, as an offset + * from the top left of the block. + * @private + */ +Blockly.BlockRendering.RenderInfo.prototype.getElemCenterline_ = function(row, elem) { + var result = row.yPos; + if (elem.isField()) { + result += (elem.height / 2); + if (row.hasInlineInput || row.hasStatement) { + result += BRC.TALL_INPUT_FIELD_OFFSET_Y; + } + } else if (elem.isInlineInput()) { + result += elem.height / 2; + } else { + result += (row.height / 2); + } + return result; +}; +/** + * Make any final changes to the rendering information object. In particular, + * store the y position of each row, and record the height of the full block. + * @private + */ +Blockly.BlockRendering.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 r = 0; r < this.rows.length; r++) { + var row = this.rows[r]; + row.yPos = yCursor; + var xCursor = 0; + if (!(row.isSpacer())) { + for (var e = 0; e < row.elements.length; e++) { + var elem = row.elements[e]; + elem.xPos = xCursor; + elem.centerline = this.getElemCenterline_(row, elem); + xCursor += elem.width; + } + } + yCursor += row.height; + } + this.blockBottom = yCursor; + + // Add padding to the bottom row if block height is less than minimum + if (yCursor < BRC.MIN_BLOCK_HEIGHT) { + this.bottomRow.height += BRC.MIN_BLOCK_HEIGHT - yCursor; + yCursor = BRC.MIN_BLOCK_HEIGHT; + } + + this.height = yCursor; +}; diff --git a/core/renderers/block_rendering_rewrite/block_rendering_constants.js b/core/renderers/block_rendering_rewrite/block_rendering_constants.js new file mode 100644 index 000000000..e6d7e69ae --- /dev/null +++ b/core/renderers/block_rendering_rewrite/block_rendering_constants.js @@ -0,0 +1,357 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2019 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a block as SVG. + * @author fenichel@google.com (Rachel Fenichel) + */ + +//'use strict'; +goog.provide('BRC'); +/* global BRC */ + +goog.require('Blockly.utils.Paths'); + + +BRC.NO_PADDING = 0; +BRC.SMALL_PADDING = 3; +BRC.MEDIUM_PADDING = 5; +BRC.MEDIUM_LARGE_PADDING = 8; +BRC.LARGE_PADDING = 10; + +// Offset from the top of the row for placing fields on inline input rows +// and statement input rows. +// Matches existing rendering (in 2019). +BRC.TALL_INPUT_FIELD_OFFSET_Y = BRC.MEDIUM_PADDING; + +BRC.HIGHLIGHT_OFFSET = 0.5; + +// The dark/shadow path in classic rendering is the same as the normal block +// path, but translated down one and right one. +BRC.DARK_PATH_OFFSET = 1; + +BRC.TAB_HEIGHT = 15; + +BRC.TAB_OFFSET_FROM_TOP = 5; + +BRC.TAB_VERTICAL_OVERLAP = 2.5; + +BRC.TAB_WIDTH = 8; + +BRC.NOTCH_WIDTH = 15; +BRC.NOTCH_HEIGHT = 4; + +// This is the minimum width of a block measuring from the end of a rounded +// corner +BRC.MIN_BLOCK_WIDTH = 12; + +BRC.EMPTY_BLOCK_SPACER_HEIGHT = 16; + + +// Offset from the left side of a block or the inside of a statement input to +// the left side of the notch. +BRC.NOTCH_OFFSET_LEFT = BRC.NOTCH_WIDTH; + +// This is the width from where a rounded corner ends to where a previous +// connection starts. +BRC.NOTCH_OFFSET_ROUNDED_CORNER_PREV = 7; + +// This is the offset from the vertical part of a statement input +// to where to start the notch, which is on the right side in LTR. +BRC.NOTCH_OFFSET_RIGHT = BRC.NOTCH_OFFSET_LEFT + BRC.NOTCH_WIDTH; + +BRC.STATEMENT_BOTTOM_SPACER = 5; +BRC.STATEMENT_INPUT_PADDING_LEFT = 20; +BRC.BETWEEN_STATEMENT_PADDING_Y = 4; + +// This is the max width of a bottom row that follows a statement input and +// has inputs inline. +BRC.MAX_BOTTOM_WIDTH = 66.5; + +/** + * Rounded corner radius. + * @const + */ +BRC.CORNER_RADIUS = 8; +/** + * Height of the top hat. + * @const + */ +BRC.START_HAT_HEIGHT = 15; + +BRC.START_HAT_WIDTH = 100; + +BRC.SPACER_DEFAULT_HEIGHT = 15; + +BRC.MIN_BLOCK_HEIGHT = 24; + +BRC.EMPTY_INLINE_INPUT_WIDTH = 22.5; + +BRC.EMPTY_INLINE_INPUT_HEIGHT = 26; + +BRC.EXTERNAL_VALUE_INPUT_WIDTH = 10; + +/** + * The height of an empty statement input. Note that in the old rendering this + * varies slightly depending on whether the block has external or inline inputs. + * In the new rendering this is consistent. It seems unlikely that the old + * behaviour was intentional. + * @const + * @type {number} + */ +BRC.EMPTY_STATEMENT_INPUT_HEIGHT = BRC.MIN_BLOCK_HEIGHT; + +BRC.EMPTY_STATEMENT_INPUT_WIDTH = 32; + +BRC.POPULATED_STATEMENT_INPUT_WIDTH = 25; + + +BRC.START_POINT = Blockly.utils.Paths.moveBy(0, 0); + +BRC.START_POINT_HIGHLIGHT = + Blockly.utils.Paths.moveBy(BRC.HIGHLIGHT_OFFSET, BRC.HIGHLIGHT_OFFSET); + +/** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the inside of a curve. + * @const + */ +BRC.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * + (BRC.CORNER_RADIUS - BRC.HIGHLIGHT_OFFSET) + BRC.HIGHLIGHT_OFFSET; + +/** + * Distance from shape edge to intersect with a curved corner at 45 degrees. + * Applies to highlighting on around the outside of a curve. + * @const + */ +BRC.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * + (BRC.CORNER_RADIUS + BRC.HIGHLIGHT_OFFSET) - BRC.HIGHLIGHT_OFFSET; + +/** + * SVG path for drawing a horizontal puzzle tab from top to bottom. + * @const + */ +BRC.TAB_PATH_DOWN = 'c 0,10 -' + BRC.TAB_WIDTH + + ',-8 -' + BRC.TAB_WIDTH + ',7.5 s ' + + BRC.TAB_WIDTH + ',-2.5 ' + BRC.TAB_WIDTH + ',7.5'; + + +/** + * SVG path for drawing a horizontal puzzle tab from top to bottom with + * highlighting from the upper-right. + * @const + */ +BRC.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'm -' + + (BRC.TAB_WIDTH * 0.97) + ',2.5 q -' + + (BRC.TAB_WIDTH * 0.05) + ',10 ' + + (BRC.TAB_WIDTH * 0.3) + ',9.5 m ' + + (BRC.TAB_WIDTH * 0.67) + ',-1.9'; + +/** + * SVG path for drawing a horizontal puzzle tab from bottom to top. + * @const + */ +BRC.TAB_PATH_UP = 'c 0,-10 -' + BRC.TAB_WIDTH + + ',8 -' + BRC.TAB_WIDTH + ',-7.5 s ' + + BRC.TAB_WIDTH + ',2.5 ' + BRC.TAB_WIDTH + ',-7.5'; + +/** + * Path of the top hat's curve. + * @const + */ +BRC.START_HAT_PATH = + Blockly.utils.Paths.curve('c', + [ + Blockly.utils.Paths.point(30, -BRC.START_HAT_HEIGHT), + Blockly.utils.Paths.point(70, -BRC.START_HAT_HEIGHT), + Blockly.utils.Paths.point(BRC.START_HAT_WIDTH, 0) + ]); +/** + * SVG path for drawing next/previous notch from left to right. + * @const + */ +BRC.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4'; + +/** + * SVG path for drawing next/previous notch from left to right with + * highlighting. + * @const + */ +BRC.NOTCH_PATH_LEFT_HIGHLIGHT = + 'h ' + BRC.HIGHLIGHT_OFFSET + ' ' + BRC.NOTCH_PATH_LEFT; + +/** + * SVG path for drawing next/previous notch from right to left. + * @const + */ +BRC.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4'; + +/** + * SVG path for drawing the top-left corner of a statement input. + * Includes the top notch, a horizontal space, and the rounded inside corner. + * @const + */ +BRC.INNER_TOP_LEFT_CORNER = + BRC.NOTCH_PATH_RIGHT + ' h -' + + (BRC.NOTCH_WIDTH - BRC.CORNER_RADIUS) + + ' a ' + BRC.CORNER_RADIUS + ',' + + BRC.CORNER_RADIUS + ' 0 0,0 -' + + BRC.CORNER_RADIUS + ',' + + BRC.CORNER_RADIUS; + +/** + * SVG path for drawing the bottom-left corner of a statement input. + * Includes the rounded inside corner. + * @const + */ +BRC.INNER_BOTTOM_LEFT_CORNER = + Blockly.utils.Paths.arc('a', '0 0,0', + BRC.CORNER_RADIUS, + Blockly.utils.Paths.point(BRC.CORNER_RADIUS, BRC.CORNER_RADIUS)); + + +/** + * SVG path for drawing highlight on the top-left corner of a statement + * input in RTL. + * @const + */ +BRC.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL = + Blockly.utils.Paths.arc('a', '0 0,0', + BRC.CORNER_RADIUS, + Blockly.utils.Paths.point( + -BRC.DISTANCE_45_OUTSIDE - BRC.HIGHLIGHT_OFFSET, + BRC.CORNER_RADIUS - BRC.DISTANCE_45_OUTSIDE)); + +/** + * SVG path for drawing highlight on the bottom-left corner of a statement + * input in RTL. + * @const + */ +BRC.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL = + Blockly.utils.Paths.arc('a', '0 0,0', + BRC.CORNER_RADIUS + BRC.HIGHLIGHT_OFFSET, + Blockly.utils.Paths.point( + BRC.CORNER_RADIUS + BRC.HIGHLIGHT_OFFSET, + BRC.CORNER_RADIUS + BRC.HIGHLIGHT_OFFSET)); + +/** + * SVG path for drawing highlight on the bottom-left corner of a statement + * input in LTR. + * @const + */ +BRC.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR = + Blockly.utils.Paths.arc('a', '0 0,0', + BRC.CORNER_RADIUS + BRC.HIGHLIGHT_OFFSET, + Blockly.utils.Paths.point( + BRC.CORNER_RADIUS - BRC.DISTANCE_45_OUTSIDE, + BRC.DISTANCE_45_OUTSIDE + BRC.HIGHLIGHT_OFFSET)); + + + +/** + * SVG start point for drawing the top-left corner. + * @const + */ +BRC.TOP_LEFT_CORNER_START = + 'm 0,' + BRC.CORNER_RADIUS; + +/** + * SVG path for drawing the rounded top-left corner. + * @const + */ +BRC.TOP_LEFT_CORNER = + 'A ' + BRC.CORNER_RADIUS + ',' + + BRC.CORNER_RADIUS + ' 0 0,1 ' + + BRC.CORNER_RADIUS + ',0'; + +BRC.BOTTOM_LEFT_CORNER = 'a' + BRC.CORNER_RADIUS + ',' + + BRC.CORNER_RADIUS + ' 0 0,1 -' + + BRC.CORNER_RADIUS + ',-' + + BRC.CORNER_RADIUS; + +BRC.BOTTOM_LEFT_CORNER_HIGHLIGHT_START = + 'M ' + BRC.DISTANCE_45_INSIDE + ', '; // follow with y pos - distance 45 inside + +BRC.BOTTOM_LEFT_CORNER_HIGHLIGHT_MID = + 'A ' + (BRC.CORNER_RADIUS - BRC.HIGHLIGHT_OFFSET) + + ',' + (BRC.CORNER_RADIUS - BRC.HIGHLIGHT_OFFSET) + + ' 0 0,1 ' + BRC.HIGHLIGHT_OFFSET + ','; // follow with y pos - corner radius + +BRC.OUTPUT_CONNECTION_HIGHLIGHT_LTR = + 'V ' + (BRC.TAB_HEIGHT + BRC.TAB_OFFSET_FROM_TOP - 1.5) + + ' m ' + (BRC.TAB_WIDTH * -0.92) + ',-0.5 ' + + 'q ' + (BRC.TAB_WIDTH * -0.19) + ',-5.5 0,-11 ' + + 'm ' + (BRC.TAB_WIDTH * 0.92) + ',1 ' + + 'V 0.5 H 1'; + +BRC.OUTPUT_CONNECTION_HIGHLIGHT_RTL = + 'M ' + (BRC.TAB_WIDTH * -0.25) + ',8.4 l ' + + (BRC.TAB_WIDTH * -0.45) + ',-2.1'; + +/** + * SVG start point for drawing the top-left corner's highlight in RTL. + * @const + */ +BRC.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL = + 'm ' + BRC.DISTANCE_45_INSIDE + ',' + + BRC.DISTANCE_45_INSIDE; + +/** + * SVG start point for drawing the top-left corner's highlight in LTR. + * @const + */ +BRC.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR = + 'm 0.5,' + (BRC.CORNER_RADIUS - 0.5); + +/** + * SVG path for drawing the highlight on the rounded top-left corner. + * @const + */ +BRC.TOP_LEFT_CORNER_HIGHLIGHT = + 'A ' + (BRC.CORNER_RADIUS - 0.5) + ',' + + (BRC.CORNER_RADIUS - 0.5) + ' 0 0,1 ' + + BRC.CORNER_RADIUS + ',0.5'; + +/** + * Path of the top hat's curve's highlight in LTR. + * @const + */ +BRC.START_HAT_HIGHLIGHT_LTR = + Blockly.utils.Paths.curve('c', + [ + Blockly.utils.Paths.point(17.8, -9.2), + Blockly.utils.Paths.point(45.3, -14.9), + Blockly.utils.Paths.point(75, -8.7) + ]) + + Blockly.utils.Paths.moveTo(100.5, 0.5); + +/** + * Path of the top hat's curve's highlight in RTL. + * @const + */ +BRC.START_HAT_HIGHLIGHT_RTL = + Blockly.utils.Paths.moveBy(25, -8.7) + + Blockly.utils.Paths.curve('c', + [ + Blockly.utils.Paths.point(29.7, -6.2), + Blockly.utils.Paths.point(57.2, -0.5), + Blockly.utils.Paths.point(75, 8.7) + ]); + diff --git a/core/renderers/block_rendering_rewrite/measurables.js b/core/renderers/block_rendering_rewrite/measurables.js new file mode 100644 index 000000000..bacfc20c4 --- /dev/null +++ b/core/renderers/block_rendering_rewrite/measurables.js @@ -0,0 +1,484 @@ +goog.provide('Blockly.BlockRendering.Measurable'); + +goog.require('BRC'); +/* global BRC */ + +/** + * The base class to represent a part of a block that takes up space during + * rendering. The constructor for each non-spacer Measurable records the size + * of the block element (e.g. field, statement input). + * @package + * @constructor + */ +Blockly.BlockRendering.Measurable = function() { + this.isInput = false; + this.width = 0; + this.height = 0; + this.type = null; + + this.xPos = 0; + this.centerline = 0; +}; + +// TODO: We may remove these helper functions if all of them end up being direct +// checks against types. + +/** + * Whether this stores information about a field. + * @return {boolean} True if this object stores information about a field. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isField = function() { + return this.type == 'field'; +}; + +/** + * Whether this stores information about a hat. + * @return {boolean} True if this object stores information about a hat. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isHat = function() { + return this.type == 'hat'; +}; + +/** + * Whether this stores information about an icon. + * @return {boolean} True if this object stores information about an icon. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isIcon = function() { + return this.type == 'icon'; +}; + +/** + * Whether this stores information about a spacer. + * @return {boolean} True if this object stores information about a spacer. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isSpacer = function() { + return this.type == 'between-row spacer' || this.type == 'in-row spacer'; +}; + +/** + * Whether this stores information about an external input. + * @return {boolean} True if this object stores information about an external + * input. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isExternalInput = function() { + return this.type == 'external value input'; +}; + +/** + * Whether this stores information about a inline input. + * @return {boolean} True if this object stores information about a inline + * input. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isInlineInput = function() { + return this.type == 'inline input'; +}; + +/** + * Whether this stores information about a statement input. + * @return {boolean} True if this object stores information about a statement + * input. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isStatementInput = function() { + return this.type == 'statement input'; +}; + +/** + * Whether this stores information about a previous connection. + * @return {boolean} True if this object stores information about a previous + * connection. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isPreviousConnection = function() { + return this.type == 'previous connection'; +}; + +/** + * Whether this stores information about a next connection. + * @return {boolean} True if this object stores information about an next + * connection. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isNextConnection = function() { + return this.type == 'next connection'; +}; + +/** + * Whether this stores information about a rounded corner. + * @return {boolean} True if this object stores information about an rounded + * corner. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isRoundedCorner = function() { + return this.type == 'round corner'; +}; + +/** + * Whether this stores information about a square corner. + * @return {boolean} True if this object stores information about an square + * corner. + * @package + */ +Blockly.BlockRendering.Measurable.prototype.isSquareCorner = function() { + return this.type == 'square corner'; +}; +/** + * The base class to represent an input that takes up space on a block + * during rendering + * @param {!Blockly.Input} input The input to measure and store information for. + * @package + * @constructor + */ +Blockly.BlockRendering.Input = function(input) { + Blockly.BlockRendering.Input.superClass_.constructor.call(this); + + this.isInput = true; + this.input = input; + this.align = input.align; + this.connectedBlock = input.connection && input.connection.targetBlock() ? + input.connection.targetBlock() : null; + + if (this.connectedBlock) { + var bBox = this.connectedBlock.getHeightWidth(); + this.connectedBlockWidth = bBox.width; + this.connectedBlockHeight = bBox.height; + } else { + this.connectedBlockWidth = 0; + this.connectedBlockHeight = 0; + } + + this.connection = input.connection; + this.connectionOffsetX = 0; + this.connectionOffsetY = 0; +}; +goog.inherits(Blockly.BlockRendering.Input, Blockly.BlockRendering.Measurable); + + +/** + * An object containing information about the space an icon takes up during + * rendering + * @param {!Blockly.Icon} icon The icon to measure and store information for. + * @package + * @constructor + */ +Blockly.BlockRendering.Icon = function(icon) { + Blockly.BlockRendering.Icon.superClass_.constructor.call(this); + this.icon = icon; + this.isVisible = icon.isVisible(); + this.type = 'icon'; + + var size = icon.getCorrectedSize(); + this.height = size.height; + this.width = size.width; +}; +goog.inherits(Blockly.BlockRendering.Icon, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space a field takes up during + * rendering + * @param {!Blockly.Field} field The field to measure and store information for. + * @param {!Blockly.Input} parentInput The parent input for the field. + * @package + * @constructor + */ +Blockly.BlockRendering.Field = function(field, parentInput) { + Blockly.BlockRendering.Field.superClass_.constructor.call(this); + this.field = field; + this.isEditable = field.isCurrentlyEditable(); + this.type = 'field'; + + var size = this.field.getCorrectedSize(); + this.height = size.height; + this.width = size.width; + this.parentInput = parentInput; +}; +goog.inherits(Blockly.BlockRendering.Field, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space an inline input takes up + * during rendering + * @param {!Blockly.Input} input The inline input to measure and store + * information for. + * @package + * @constructor + */ +Blockly.BlockRendering.InlineInput = function(input) { + Blockly.BlockRendering.InlineInput.superClass_.constructor.call(this, input); + this.type = 'inline input'; + + if (!this.connectedBlock) { + this.height = BRC.EMPTY_INLINE_INPUT_HEIGHT; + this.width = BRC.EMPTY_INLINE_INPUT_WIDTH; + } else { + // We allow the dark path to show on the parent block so that the child + // block looks embossed. This takes up an extra pixel in both x and y. + this.width = this.connectedBlockWidth + BRC.TAB_WIDTH + BRC.DARK_PATH_OFFSET; + this.height = this.connectedBlockHeight + BRC.DARK_PATH_OFFSET; + } +}; +goog.inherits(Blockly.BlockRendering.InlineInput, Blockly.BlockRendering.Input); + +/** + * An object containing information about the space a statement input takes up + * during rendering + * @param {!Blockly.Input} input The statement input to measure and store + * information for. + * @package + * @constructor + */ +Blockly.BlockRendering.StatementInput = function(input) { + Blockly.BlockRendering.StatementInput.superClass_.constructor.call(this, input); + this.type = 'statement input'; + + if (!this.connectedBlock) { + this.height = BRC.EMPTY_STATEMENT_INPUT_HEIGHT; + this.width = BRC.EMPTY_STATEMENT_INPUT_WIDTH; + } else { + this.width = BRC.POPULATED_STATEMENT_INPUT_WIDTH; + this.height = this.connectedBlockHeight + BRC.STATEMENT_BOTTOM_SPACER; + if (this.connectedBlock.nextConnection) { + this.height -= BRC.NOTCH_HEIGHT; + } + } +}; +goog.inherits(Blockly.BlockRendering.StatementInput, + Blockly.BlockRendering.Input); + +/** + * An object containing information about the space an external value input + * takes up during rendering + * @param {!Blockly.Input} input The external value input to measure and store + * information for. + * @package + * @constructor + */ +Blockly.BlockRendering.ExternalValueInput = function(input) { + Blockly.BlockRendering.ExternalValueInput.superClass_.constructor.call(this, input); + this.type = 'external value input'; + + if (!this.connectedBlock) { + this.height = BRC.TAB_HEIGHT; + } else { + this.height = this.connectedBlockHeight - 2 * BRC.TAB_OFFSET_FROM_TOP; + } + this.width = BRC.EXTERNAL_VALUE_INPUT_WIDTH; +}; +goog.inherits(Blockly.BlockRendering.ExternalValueInput, + Blockly.BlockRendering.Input); + +/** + * An object containing information about the space a previous connection takes + * up during rendering. + * @package + * @constructor + */ +Blockly.BlockRendering.PreviousConnection = function() { + Blockly.BlockRendering.PreviousConnection.superClass_.constructor.call(this); + this.type = 'previous connection'; + this.height = BRC.NOTCH_HEIGHT; + this.width = BRC.NOTCH_WIDTH; + +}; +goog.inherits(Blockly.BlockRendering.PreviousConnection, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space a next connection takes + * up during rendering. + * @package + * @constructor + */ +Blockly.BlockRendering.NextConnection = function() { + Blockly.BlockRendering.NextConnection.superClass_.constructor.call(this); + this.type = 'next connection'; + this.height = BRC.NOTCH_HEIGHT; + this.width = BRC.NOTCH_WIDTH; +}; +goog.inherits(Blockly.BlockRendering.NextConnection, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space a hat takes up during + * rendering. + * @package + * @constructor + */ +Blockly.BlockRendering.Hat = function() { + Blockly.BlockRendering.Hat.superClass_.constructor.call(this); + this.type = 'hat'; + this.height = BRC.NO_PADDING; + this.width = BRC.START_HAT_WIDTH; + +}; +goog.inherits(Blockly.BlockRendering.Hat, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space a square corner takes up + * during rendering. + * @package + * @constructor + */ +Blockly.BlockRendering.SquareCorner = function() { + Blockly.BlockRendering.SquareCorner.superClass_.constructor.call(this); + this.type = 'square corner'; + this.height = BRC.NOTCH_HEIGHT; + this.width = BRC.NO_PADDING; + +}; +goog.inherits(Blockly.BlockRendering.SquareCorner, Blockly.BlockRendering.Measurable); + +/** + * An object containing information about the space a rounded corner takes up + * during rendering. + * @package + * @constructor + */ +Blockly.BlockRendering.RoundCorner = function() { + Blockly.BlockRendering.RoundCorner.superClass_.constructor.call(this); + this.type = 'round corner'; + this.width = BRC.CORNER_RADIUS; + // The rounded corner extends into the next row by 4 so we only take the + // height that is aligned with this row. + this.height = BRC.NOTCH_HEIGHT; + +}; +goog.inherits(Blockly.BlockRendering.RoundCorner, Blockly.BlockRendering.Measurable); + + +Blockly.BlockRendering.Row = function() { + this.type = 'row'; + this.yPos = 0; + this.elements = []; + this.width = 0; + this.height = 0; + + this.hasExternalInput = false; + this.hasStatement = false; + this.hasInlineInput = false; + this.hasDummyInput = false; +}; + +Blockly.BlockRendering.Row.prototype.isSpacer = function() { + return false; +}; + +Blockly.BlockRendering.Row.prototype.measure = function() { + var connectedBlockWidths = 0; + for (var e = 0; e < this.elements.length; e++) { + var elem = this.elements[e]; + this.width += elem.width; + if (elem.isInput && + (elem.type == 'statement input' || elem.type == 'external value input')) { + connectedBlockWidths += elem.connectedBlockWidth; + } + if (!(elem.isSpacer())) { + this.height = Math.max(this.height, elem.height); + } + } + this.widthWithConnectedBlocks = this.width + connectedBlockWidths; +}; + +Blockly.BlockRendering.Row.prototype.getLastInput = function() { + // There's always a spacer after the last input, unless there are no inputs. + if (this.elements.length > 1) { + var elem = this.elements[this.elements.length - 2]; + if (elem.isInput) { + return elem; + } else if (elem.isField()) { + return elem.parentInput; + } + } + // Return null if there are no inputs. + return null; +}; + +Blockly.BlockRendering.Row.prototype.getFirstSpacer = function() { + return this.elements[0]; +}; + +Blockly.BlockRendering.Row.prototype.getLastSpacer = function() { + return this.elements[this.elements.length - 1]; +}; + +Blockly.BlockRendering.BetweenRowSpacer = function(height, width) { + this.type = 'between-row spacer'; + this.width = width; + this.height = height; + this.followsStatement = false; +}; +goog.inherits(Blockly.BlockRendering.BetweenRowSpacer, + Blockly.BlockRendering.Measurable); + +Blockly.BlockRendering.InRowSpacer = function(width) { + this.type = 'in-row spacer'; + this.width = width; + this.height = BRC.SPACER_DEFAULT_HEIGHT; +}; +goog.inherits(Blockly.BlockRendering.InRowSpacer, + Blockly.BlockRendering.Measurable); + +/** + * An object containing information about what elements are in the top row of a + * block as well as spacing information for the top row. + * Elements in a top row can consist of corners, hats and previous connections. + * @param {[type]} block [description] + * @package + */ +Blockly.BlockRendering.TopRow = function(block) { + Blockly.BlockRendering.TopRow.superClass_.constructor.call(this); + + this.elements = []; + this.type = 'top row'; + + this.hasPreviousConnection = !!block.previousConnection; + this.connection = block.previousConnection; + + var precedesStatement = block.inputList.length && + block.inputList[0].type == Blockly.NEXT_STATEMENT; + + // This is the minimum height for the row. If one of its elements has a greater + // height it will be overwritten in the compute pass. + if (precedesStatement) { + this.height = BRC.LARGE_PADDING; + } else { + this.height = BRC.MEDIUM_PADDING; + } +}; +goog.inherits(Blockly.BlockRendering.TopRow, Blockly.BlockRendering.Row); + + +Blockly.BlockRendering.TopRow.prototype.isSpacer = function() { + return true; +}; + +Blockly.BlockRendering.BottomRow = function(block) { + Blockly.BlockRendering.BottomRow.superClass_.constructor.call(this); + this.type = 'bottom row'; + this.hasNextConnection = !!block.nextConnection; + this.connection = block.nextConnection; + + var followsStatement = + block.inputList.length && + block.inputList[block.inputList.length - 1].type == Blockly.NEXT_STATEMENT; + this.hasFixedWidth = followsStatement && block.getInputsInline(); + + // This is the minimum height for the row. If one of it's elements has a greater + // height it will be overwritten in the compute pass. + if (followsStatement) { + this.height = BRC.LARGE_PADDING; + } else { + this.height = BRC.NOTCH_HEIGHT; + } + +}; +goog.inherits(Blockly.BlockRendering.BottomRow, + Blockly.BlockRendering.Row); + +Blockly.BlockRendering.BottomRow.prototype.isSpacer = function() { + return true; +}; + From e473af84d823eed7b858b1add9e72014349872bd Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 22 Jul 2019 16:32:54 -0700 Subject: [PATCH 2/2] Rename function --- core/renderers/block_rendering_rewrite/block_render_draw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/renderers/block_rendering_rewrite/block_render_draw.js b/core/renderers/block_rendering_rewrite/block_render_draw.js index da9c84b60..515611cda 100644 --- a/core/renderers/block_rendering_rewrite/block_render_draw.js +++ b/core/renderers/block_rendering_rewrite/block_render_draw.js @@ -267,7 +267,7 @@ Blockly.BlockRendering.Drawer.prototype.drawInternals_ = function() { * @return {number} How far to offset the field in the x direction. * @private */ -Blockly.BlockRendering.Drawer.prototype.dealWithJackassFields_ = function(field) { +Blockly.BlockRendering.Drawer.prototype.dealWithOffsetFields_ = function(field) { if (field instanceof Blockly.FieldDropdown || field instanceof Blockly.FieldTextInput || field instanceof Blockly.FieldColour @@ -300,7 +300,7 @@ Blockly.BlockRendering.Drawer.prototype.layoutField_ = function(fieldInfo) { svgGroup.setAttribute('transform', 'translate(' + xPos + ',' + yPos + ')'); fieldInfo.icon.computeIconLocation(); } else { - xPos += this.dealWithJackassFields_(fieldInfo.field); + xPos += this.dealWithOffsetFields_(fieldInfo.field); svgGroup.setAttribute('transform', 'translate(' + xPos + ',' + yPos + ')'); }