/** * @license * Visual Blocks Editor * * Copyright 2012 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 fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.BlockSvg.render'); goog.require('Blockly.BlockSvg'); goog.require('Blockly.scratchBlocksUtils'); goog.require('Blockly.utils'); // UI constants for rendering blocks. /** * Grid unit to pixels conversion * @const */ Blockly.BlockSvg.GRID_UNIT = 4; /** * Horizontal space between elements. * @const */ Blockly.BlockSvg.SEP_SPACE_X = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Vertical space between elements. * @const */ Blockly.BlockSvg.SEP_SPACE_Y = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of a block. * @const */ Blockly.BlockSvg.MIN_BLOCK_X = 16 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of a block with output (reporters). * @const */ Blockly.BlockSvg.MIN_BLOCK_X_OUTPUT = 12 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of a shadow block with output (single fields). * @const */ Blockly.BlockSvg.MIN_BLOCK_X_SHADOW_OUTPUT = 10 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum height of a block. * @const */ Blockly.BlockSvg.MIN_BLOCK_Y = 12 * Blockly.BlockSvg.GRID_UNIT; /** * Height of extra row after a statement input. * @const */ Blockly.BlockSvg.EXTRA_STATEMENT_ROW_Y = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of a C- or E-shaped block. * @const */ Blockly.BlockSvg.MIN_BLOCK_X_WITH_STATEMENT = 40 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum height of a shadow block with output and a single field. * This is used for shadow blocks that only contain a field - which are smaller than even reporters. * @const */ Blockly.BlockSvg.MIN_BLOCK_Y_SINGLE_FIELD_OUTPUT = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum height of a non-shadow block with output, i.e. a reporter. * @const */ Blockly.BlockSvg.MIN_BLOCK_Y_REPORTER = 10 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum space for a statement input height. * @const */ Blockly.BlockSvg.MIN_STATEMENT_INPUT_HEIGHT = 6 * Blockly.BlockSvg.GRID_UNIT; /** * Width of vertical notch. * @const */ Blockly.BlockSvg.NOTCH_WIDTH = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Height of vertical notch. * @const */ Blockly.BlockSvg.NOTCH_HEIGHT = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Rounded corner radius. * @const */ Blockly.BlockSvg.CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of statement input edge on the left, in px. * @const */ Blockly.BlockSvg.STATEMENT_INPUT_EDGE_WIDTH = 4 * Blockly.BlockSvg.GRID_UNIT; /** * Inner space between edge of statement input and notch. * @const */ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Height of the top hat. * @const */ Blockly.BlockSvg.START_HAT_HEIGHT = 16; /** * Height of the vertical separator line for icons that appear at the left edge * of a block, such as extension icons. * @const */ Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT; /** * Path of the top hat's curve. * @const */ Blockly.BlockSvg.START_HAT_PATH = 'c 25,-22 71,-22 96,0'; /** * SVG path for drawing next/previous notch from left to right. * @const */ Blockly.BlockSvg.NOTCH_PATH_LEFT = ( 'c 2,0 3,1 4,2 ' + 'l 4,4 ' + 'c 1,1 2,2 4,2 ' + 'h 12 ' + 'c 2,0 3,-1 4,-2 ' + 'l 4,-4 ' + 'c 1,-1 2,-2 4,-2' ); /** * SVG path for drawing next/previous notch from right to left. * @const */ Blockly.BlockSvg.NOTCH_PATH_RIGHT = ( 'c -2,0 -3,1 -4,2 ' + 'l -4,4 ' + 'c -1,1 -2,2 -4,2 ' + 'h -12 ' + 'c -2,0 -3,-1 -4,-2 ' + 'l -4,-4 ' + 'c -1,-1 -2,-2 -4,-2' ); /** * Amount of padding before the notch. * @const */ Blockly.BlockSvg.NOTCH_START_PADDING = 3 * Blockly.BlockSvg.GRID_UNIT; /** * SVG start point for drawing the top-left corner. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_START = 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for drawing the rounded top-left corner. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER = 'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.CORNER_RADIUS + ',0'; /** * SVG path for drawing the rounded top-right corner. * @const */ Blockly.BlockSvg.TOP_RIGHT_CORNER = 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for drawing the rounded bottom-right corner. * @const */ Blockly.BlockSvg.BOTTOM_RIGHT_CORNER = ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for drawing the rounded bottom-left corner. * @const */ Blockly.BlockSvg.BOTTOM_LEFT_CORNER = 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' + Blockly.BlockSvg.CORNER_RADIUS + ',-' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for drawing the top-left corner of a statement input. * @const */ Blockly.BlockSvg.INNER_TOP_LEFT_CORNER = ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for drawing the bottom-left corner of a statement input. * Includes the rounded inside corner. * @const */ Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER = 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' + Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG path for an empty hexagonal input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_HEXAGONAL = 'M ' + 4 * Blockly.BlockSvg.GRID_UNIT + ',0 ' + ' h ' + 4 * Blockly.BlockSvg.GRID_UNIT + ' l ' + 4 * Blockly.BlockSvg.GRID_UNIT + ',' + 4 * Blockly.BlockSvg.GRID_UNIT + ' l ' + -4 * Blockly.BlockSvg.GRID_UNIT + ',' + 4 * Blockly.BlockSvg.GRID_UNIT + ' h ' + -4 * Blockly.BlockSvg.GRID_UNIT + ' l ' + -4 * Blockly.BlockSvg.GRID_UNIT + ',' + -4 * Blockly.BlockSvg.GRID_UNIT + ' l ' + 4 * Blockly.BlockSvg.GRID_UNIT + ',' + -4 * Blockly.BlockSvg.GRID_UNIT + ' z'; /** * Width of empty boolean input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_HEXAGONAL_WIDTH = 12 * Blockly.BlockSvg.GRID_UNIT; /** * SVG path for an empty square input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_SQUARE = Blockly.BlockSvg.TOP_LEFT_CORNER_START + Blockly.BlockSvg.TOP_LEFT_CORNER + ' h ' + (12 * Blockly.BlockSvg.GRID_UNIT - 2 * Blockly.BlockSvg.CORNER_RADIUS) + Blockly.BlockSvg.TOP_RIGHT_CORNER + ' v ' + (8 * Blockly.BlockSvg.GRID_UNIT - 2 * Blockly.BlockSvg.CORNER_RADIUS) + Blockly.BlockSvg.BOTTOM_RIGHT_CORNER + ' h ' + (-12 * Blockly.BlockSvg.GRID_UNIT + 2 * Blockly.BlockSvg.CORNER_RADIUS) + Blockly.BlockSvg.BOTTOM_LEFT_CORNER + ' z'; /** * Width of empty square input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_SQUARE_WIDTH = 10 * Blockly.BlockSvg.GRID_UNIT; /** * SVG path for an empty round input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_ROUND = 'M ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ',0' + ' h ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ' a ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ' ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ' 0 0 1 0 ' + (8 * Blockly.BlockSvg.GRID_UNIT) + ' h ' + (-4 * Blockly.BlockSvg.GRID_UNIT) + ' a ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ' ' + (4 * Blockly.BlockSvg.GRID_UNIT) + ' 0 0 1 0 -' + (8 * Blockly.BlockSvg.GRID_UNIT) + ' z'; /** * Width of empty round input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_ROUND_WIDTH = 12 * Blockly.BlockSvg.GRID_UNIT; /** * Height of empty input shape. * @const */ Blockly.BlockSvg.INPUT_SHAPE_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Height of user inputs * @const */ Blockly.BlockSvg.FIELD_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Width of user inputs * @const */ Blockly.BlockSvg.FIELD_WIDTH = 6 * Blockly.BlockSvg.GRID_UNIT; /** * Editable field padding (left/right of the text). * @const */ Blockly.BlockSvg.EDITABLE_FIELD_PADDING = 6; /** * Square box field padding (left/right of the text). * @const */ Blockly.BlockSvg.BOX_FIELD_PADDING = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Drop-down arrow padding. * @const */ Blockly.BlockSvg.DROPDOWN_ARROW_PADDING = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Minimum width of user inputs during editing * @const */ Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT = 8 * Blockly.BlockSvg.GRID_UNIT; /** * Maximum width of user inputs during editing * @const */ Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT = Infinity; /** * Maximum height of user inputs during editing * @const */ Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT = Blockly.BlockSvg.FIELD_HEIGHT; /** * Top padding of user inputs * @const */ Blockly.BlockSvg.FIELD_TOP_PADDING = 0.5 * Blockly.BlockSvg.GRID_UNIT; /** * Corner radius of number inputs * @const */ Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT; /** * Corner radius of text inputs * @const */ Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT; /** * Default radius for a field, in px. * @const */ Blockly.BlockSvg.FIELD_DEFAULT_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT; /** * Max text display length for a field (per-horizontal/vertical) * @const */ Blockly.BlockSvg.MAX_DISPLAY_LENGTH = Infinity; /** * Minimum X of inputs and fields for blocks with a previous connection. * Ensures that inputs will not overlap with the top notch of blocks. * @const */ Blockly.BlockSvg.INPUT_AND_FIELD_MIN_X = 12 * Blockly.BlockSvg.GRID_UNIT; /** * Vertical padding around inline elements. * @const */ Blockly.BlockSvg.INLINE_PADDING_Y = 1 * Blockly.BlockSvg.GRID_UNIT; /** * Point size of text field before animation. Must match size in CSS. * See implementation in field_textinput. */ Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL = 12; /** * Point size of text field after animation. * See implementation in field_textinput. */ Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_FINAL = 12; /** * Whether text fields are allowed to expand past their truncated block size. * @const{boolean} */ Blockly.BlockSvg.FIELD_TEXTINPUT_EXPAND_PAST_TRUNCATION = false; /** * Whether text fields should animate their positioning. * @const{boolean} */ Blockly.BlockSvg.FIELD_TEXTINPUT_ANIMATE_POSITIONING = false; /** * Map of output/input shapes and the amount they should cause a block to be padded. * Outer key is the outer shape, inner key is the inner shape. * When a block with the outer shape contains an input block with the inner shape * on its left or right edge, that side is extended by the padding specified. * See also: `Blockly.BlockSvg.computeOutputPadding_`. */ Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING = { 1: { // Outer shape: hexagon. 0: 5 * Blockly.BlockSvg.GRID_UNIT, // Field in hexagon. 1: 2 * Blockly.BlockSvg.GRID_UNIT, // Hexagon in hexagon. 2: 5 * Blockly.BlockSvg.GRID_UNIT, // Round in hexagon. 3: 5 * Blockly.BlockSvg.GRID_UNIT // Square in hexagon. }, 2: { // Outer shape: round. 0: 3 * Blockly.BlockSvg.GRID_UNIT, // Field in round. 1: 3 * Blockly.BlockSvg.GRID_UNIT, // Hexagon in round. 2: 1 * Blockly.BlockSvg.GRID_UNIT, // Round in round. 3: 2 * Blockly.BlockSvg.GRID_UNIT // Square in round. }, 3: { // Outer shape: square. 0: 2 * Blockly.BlockSvg.GRID_UNIT, // Field in square. 1: 2 * Blockly.BlockSvg.GRID_UNIT, // Hexagon in square. 2: 2 * Blockly.BlockSvg.GRID_UNIT, // Round in square. 3: 2 * Blockly.BlockSvg.GRID_UNIT // Square in square. } }; /** * Corner radius of the hat on the define block. * @const */ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS = 5 * Blockly.BlockSvg.GRID_UNIT; /** * SVG path for drawing the rounded top-left corner. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT = 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; /** * SVG path for drawing the rounded top-left corner. * @const */ Blockly.BlockSvg.TOP_RIGHT_CORNER_DEFINE_HAT = 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; /** * Padding on the right side of the internal block on the define block. * @const */ Blockly.BlockSvg.DEFINE_BLOCK_PADDING_RIGHT = 2 * Blockly.BlockSvg.GRID_UNIT; /** * Change the colour of a block. */ Blockly.BlockSvg.prototype.updateColour = function() { var strokeColour = this.getColourTertiary(); var renderShadowed = this.isShadow() && !Blockly.scratchBlocksUtils.isShadowArgumentReporter(this); if (renderShadowed && this.parentBlock_) { // Pull shadow block stroke colour from parent block's tertiary if possible. strokeColour = this.parentBlock_.getColourTertiary(); // Special case: if we contain a colour field, set to a special stroke colour. if (this.inputList[0] && this.inputList[0].fieldRow[0] && (this.inputList[0].fieldRow[0] instanceof Blockly.FieldColour || this.inputList[0].fieldRow[0] instanceof Blockly.FieldColourSlider)) { strokeColour = Blockly.Colours.colourPickerStroke; } } // Render block stroke this.svgPath_.setAttribute('stroke', strokeColour); // Render block fill if (this.isGlowingBlock_ || renderShadowed) { // Use the block's shadow colour if possible. if (this.getShadowColour()) { var fillColour = this.getShadowColour(); } else { var fillColour = this.getColourSecondary(); } } else { var fillColour = this.getColour(); } this.svgPath_.setAttribute('fill', fillColour); // Render opacity this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); // Update colours of input shapes. for (var i = 0, input; input = this.inputList[i]; i++) { if (input.outlinePath) { input.outlinePath.setAttribute('fill', this.getColourTertiary()); } } // Render icon(s) if applicable var icons = this.getIcons(); for (var i = 0; i < icons.length; i++) { icons[i].updateColour(); } // Bump every dropdown to change its colour. for (var x = 0, input; input = this.inputList[x]; x++) { for (var y = 0, field; field = input.fieldRow[y]; y++) { field.setText(null); } } }; /** * Visual effect to show that if the dragging block is dropped, this block will * be replaced. If a shadow block it will disappear. Otherwise it will bump. * @param {boolean} add True if highlighting should be added. */ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { this.svgPath_.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } }; /** * Visual effect to show that if the dragging block is dropped it will connect * to this input. * @param {Blockly.Connection} conn The connection on the input to highlight. * @param {boolean} add True if highlighting should be added. */ Blockly.BlockSvg.prototype.highlightShapeForInput = function(conn, add) { var input = this.getInputWithConnection(conn); if (!input) { throw 'No input found for the connection'; } if (!input.outlinePath) { return; } if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; input.outlinePath.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { input.outlinePath.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } }; /** * Returns a bounding box describing the dimensions of this block * and any blocks stacked below it. * @return {!{height: number, width: number}} Object with height and width properties. */ Blockly.BlockSvg.prototype.getHeightWidth = function() { var height = this.height; var width = this.width; // Recursively add size of subsequent blocks. var nextBlock = this.getNextBlock(); if (nextBlock) { var nextHeightWidth = nextBlock.getHeightWidth(); height += nextHeightWidth.height; height -= Blockly.BlockSvg.NOTCH_HEIGHT; // Exclude height of connected notch. width = Math.max(width, nextHeightWidth.width); } return {height: height, width: width}; }; /** * Render the block. * Lays out and reflows a block based on its contents and settings. * @param {boolean=} opt_bubble If false, just render this block. * If true, also render block's parent, grandparent, etc. Defaults to true. */ Blockly.BlockSvg.prototype.render = function(opt_bubble) { Blockly.Field.startCache(); this.rendered = true; var cursorX = Blockly.BlockSvg.SEP_SPACE_X; if (this.RTL) { cursorX = -cursorX; } // Move the icons into position. var icons = this.getIcons(); var scratchCommentIcon = null; for (var i = 0; i < icons.length; i++) { if (icons[i] instanceof Blockly.ScratchBlockComment) { // Don't render scratch block comment icon until // after the inputs scratchCommentIcon = icons[i]; } else { cursorX = icons[i].renderIcon(cursorX); } } cursorX += this.RTL ? Blockly.BlockSvg.SEP_SPACE_X : -Blockly.BlockSvg.SEP_SPACE_X; // If there are no icons, cursorX will be 0, otherwise it will be the // width that the first label needs to move over by. // If this is an extension reporter block, add a horizontal offset. if (this.isScratchExtension && this.outputConnection) { cursorX += this.RTL ? -Blockly.BlockSvg.GRID_UNIT : Blockly.BlockSvg.GRID_UNIT; } var inputRows = this.renderCompute_(cursorX); this.renderDraw_(cursorX, inputRows); this.renderMoveConnections_(); this.renderClassify_(); // Position the Scratch Block Comment Icon at the end of the block if (scratchCommentIcon) { var iconX = this.RTL ? -inputRows.rightEdge : inputRows.rightEdge; var inputMarginY = inputRows[0].height / 2; scratchCommentIcon.renderIcon(iconX, inputMarginY); } if (opt_bubble !== false) { // Render all blocks above this one (propagate a reflow). var parentBlock = this.getParent(); if (parentBlock) { parentBlock.render(true); } else { // Top-most block. Fire an event to allow scrollbars to resize. Blockly.resizeSvgContents(this.workspace); } } Blockly.Field.stopCache(); }; /** * Render a list of fields starting at the specified location. * @param {!Array.<!Blockly.Field>} fieldList List of fields. * @param {number} cursorX X-coordinate to start the fields. * @param {number} cursorY Y-coordinate around which fields are centered. * @return {number} X-coordinate of the end of the field row (plus a gap). * @private */ Blockly.BlockSvg.prototype.renderFields_ = function(fieldList, cursorX, cursorY) { if (this.RTL) { cursorX = -cursorX; } for (var t = 0, field; field = fieldList[t]; t++) { var root = field.getSvgRoot(); if (!root) { continue; } // In blocks with a notch, fields should be bumped to a min X, // to avoid overlapping with the notch. Label and image fields are // excluded. if (this.previousConnection && !(field instanceof Blockly.FieldLabel) && !(field instanceof Blockly.FieldImage)) { cursorX = this.RTL ? Math.min(cursorX, -Blockly.BlockSvg.INPUT_AND_FIELD_MIN_X) : Math.max(cursorX, Blockly.BlockSvg.INPUT_AND_FIELD_MIN_X); } // Offset the field upward by half its height. // This vertically centers the fields around cursorY. var yOffset = -field.getSize().height / 2; // If this is an extension block, and this field is the first field, and // it is an image field, and this block has a previous connection, bump // the image down by one grid unit to align it vertically. if (this.isScratchExtension && (field === this.inputList[0].fieldRow[0]) && (field instanceof Blockly.FieldImage) && this.previousConnection) { yOffset += Blockly.BlockSvg.GRID_UNIT; } // If this is an extension hat block, adjust the height of the vertical // separator without adjusting the field height. The effect is to move // the bottom end of the line up one grid unit. if (this.isScratchExtension && !this.previousConnection && this.nextConnection && field instanceof Blockly.FieldVerticalSeparator) { field.setLineHeight(Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT - Blockly.BlockSvg.GRID_UNIT); } var translateX, translateY; var scale = ''; if (this.RTL) { cursorX -= field.renderSep + field.renderWidth; translateX = cursorX; translateY = cursorY + yOffset; if (field.renderWidth) { cursorX -= Blockly.BlockSvg.SEP_SPACE_X; } } else { translateX = cursorX + field.renderSep; translateY = cursorY + yOffset; if (field.renderWidth) { cursorX += field.renderSep + field.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; } } if (this.RTL && field instanceof Blockly.FieldImage && field.getFlipRTL()) { scale = 'scale(-1 1)'; translateX += field.renderWidth; } root.setAttribute('transform', 'translate(' + translateX + ', ' + translateY + ') ' + scale); // Fields are invisible on insertion marker. if (this.isInsertionMarker()) { root.setAttribute('display', 'none'); } } return this.RTL ? -cursorX : cursorX; }; /** * Computes the height and widths for each row and field. * @param {number} iconWidth Offset of first row due to icons. * @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing * position information. * @private */ Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) { var inputList = this.inputList; var inputRows = []; // Block will be drawn from 0 (left edge) to rightEdge, in px. inputRows.rightEdge = 0; // Drawn from 0 to bottomEdge vertically. inputRows.bottomEdge = 0; var fieldValueWidth = 0; // Width of longest external value field. var fieldStatementWidth = 0; // Width of longest statement field. var hasValue = false; var hasStatement = false; var hasDummy = false; var lastType = undefined; // Previously created row, for special-casing row heights on C- and E- shaped blocks. var previousRow; for (var i = 0, input; input = inputList[i]; i++) { if (!input.isVisible()) { continue; } var isSecondInputOnProcedure = this.type == 'procedures_definition' && lastType && lastType == Blockly.NEXT_STATEMENT; var row; // Don't create a new row for the second dummy input on a procedure block. // See github.com/LLK/scratch-blocks/issues/1658 // In all other cases, statement and value inputs catch all preceding dummy // inputs, and cause a line break before following inputs. if (!isSecondInputOnProcedure && (!lastType || lastType == Blockly.NEXT_STATEMENT || input.type == Blockly.NEXT_STATEMENT)) { lastType = input.type; row = this.createRowForInput_(input); inputRows.push(row); } else { row = inputRows[inputRows.length - 1]; } row.push(input); // Compute minimum dimensions for this input. input.renderHeight = this.computeInputHeight_(input, row, previousRow); input.renderWidth = this.computeInputWidth_(input); // If the input is a statement input, determine if a notch // should be drawn at the inner bottom of the C. row.statementNotchAtBottom = true; if (input.connection && input.connection.type === Blockly.NEXT_STATEMENT) { var linkedBlock = input.connection.targetBlock(); if (linkedBlock && !linkedBlock.lastConnectionInStack()) { row.statementNotchAtBottom = false; } } // Expand input size. if (input.connection) { var linkedBlock = input.connection.targetBlock(); var paddedHeight = 0; var paddedWidth = 0; if (linkedBlock) { // A block is connected to the input - use its size. var bBox = linkedBlock.getHeightWidth(); paddedHeight = bBox.height; paddedWidth = bBox.width; } else { // No block connected - use the size of the rendered empty input shape. paddedHeight = Blockly.BlockSvg.INPUT_SHAPE_HEIGHT; } if (input.connection.type === Blockly.INPUT_VALUE) { paddedHeight += 2 * Blockly.BlockSvg.INLINE_PADDING_Y; } if (input.connection.type === Blockly.NEXT_STATEMENT) { // Subtract height of notch, only if the last block in the stack has a next connection. if (row.statementNotchAtBottom) { paddedHeight -= Blockly.BlockSvg.NOTCH_HEIGHT; } } input.renderHeight = Math.max(input.renderHeight, paddedHeight); input.renderWidth = Math.max(input.renderWidth, paddedWidth); } row.height = Math.max(row.height, input.renderHeight); input.fieldWidth = 0; if (inputRows.length == 1) { // The first row gets shifted to accommodate any icons. input.fieldWidth += this.RTL ? -iconWidth : iconWidth; } var previousFieldEditable = false; for (var j = 0, field; field = input.fieldRow[j]; j++) { if (j != 0) { input.fieldWidth += Blockly.BlockSvg.SEP_SPACE_X; } // Get the dimensions of the field. var fieldSize = field.getSize(); field.renderWidth = fieldSize.width; field.renderSep = (previousFieldEditable && field.EDITABLE) ? Blockly.BlockSvg.SEP_SPACE_X : 0; // See github.com/LLK/scratch-blocks/issues/1658 if (!isSecondInputOnProcedure) { input.fieldWidth += field.renderWidth + field.renderSep; } row.height = Math.max(row.height, fieldSize.height); previousFieldEditable = field.EDITABLE; } if (row.type != Blockly.BlockSvg.INLINE) { if (row.type == Blockly.NEXT_STATEMENT) { hasStatement = true; fieldStatementWidth = Math.max(fieldStatementWidth, input.fieldWidth); } else { if (row.type == Blockly.INPUT_VALUE) { hasValue = true; } else if (row.type == Blockly.DUMMY_INPUT) { hasDummy = true; } fieldValueWidth = Math.max(fieldValueWidth, input.fieldWidth); } } previousRow = row; } // Compute padding for output blocks. // Data is attached to the row. this.computeOutputPadding_(inputRows); // Compute the statement edge. // This is the width of a block where statements are nested. inputRows.statementEdge = Blockly.BlockSvg.STATEMENT_INPUT_EDGE_WIDTH + fieldStatementWidth; // Compute the preferred right edge. inputRows.rightEdge = this.computeRightEdge_(inputRows.rightEdge, hasStatement); // Bottom edge is sum of row heights for (var i = 0; i < inputRows.length; i++) { inputRows.bottomEdge += inputRows[i].height; } inputRows.hasValue = hasValue; inputRows.hasStatement = hasStatement; inputRows.hasDummy = hasDummy; return inputRows; }; /** * Compute the minimum width of this input based on the connection type and * outputs. * @param {!Blockly.Input} input The input to measure. * @return {number} the computed width of this input. * @private */ Blockly.BlockSvg.prototype.computeInputWidth_ = function(input) { // Empty input shape widths. if (input.type == Blockly.INPUT_VALUE && (!input.connection || !input.connection.isConnected())) { switch (input.connection.getOutputShape()) { case Blockly.OUTPUT_SHAPE_SQUARE: return Blockly.BlockSvg.INPUT_SHAPE_SQUARE_WIDTH; case Blockly.OUTPUT_SHAPE_ROUND: return Blockly.BlockSvg.INPUT_SHAPE_ROUND_WIDTH; case Blockly.OUTPUT_SHAPE_HEXAGONAL: return Blockly.BlockSvg.INPUT_SHAPE_HEXAGONAL_WIDTH; default: return 0; } } else { return 0; } }; /** * Compute the minimum height of this input. * @param {!Blockly.Input} input The input to measure. * @param {!Object} row The row of the block that is currently being measured. * @param {!Object} previousRow The previous row of the block, which was just * measured. * @return {number} the computed height of this input. * @private */ Blockly.BlockSvg.prototype.computeInputHeight_ = function(input, row, previousRow) { if (this.inputList.length === 1 && this.outputConnection && (this.isShadow() && !Blockly.scratchBlocksUtils.isShadowArgumentReporter(this))) { // "Lone" field blocks are smaller. return Blockly.BlockSvg.MIN_BLOCK_Y_SINGLE_FIELD_OUTPUT; } else if (this.outputConnection) { // If this is an extension reporter block, make it taller. if (this.isScratchExtension) { return Blockly.BlockSvg.MIN_BLOCK_Y_REPORTER + 2 * Blockly.BlockSvg.GRID_UNIT; } // All other reporters. return Blockly.BlockSvg.MIN_BLOCK_Y_REPORTER; } else if (row.type == Blockly.NEXT_STATEMENT) { // Statement input. return Blockly.BlockSvg.MIN_STATEMENT_INPUT_HEIGHT; } else if (previousRow && previousRow.type == Blockly.NEXT_STATEMENT) { // Extra row for below statement input. return Blockly.BlockSvg.EXTRA_STATEMENT_ROW_Y; } else { // If this is an extension block, and it has a previous connection, // make it taller. if (this.isScratchExtension && this.previousConnection) { return Blockly.BlockSvg.MIN_BLOCK_Y + 2 * Blockly.BlockSvg.GRID_UNIT; } // All other blocks. return Blockly.BlockSvg.MIN_BLOCK_Y; } }; /** * Create a row for an input and associated fields. * @param {!Blockly.Input} input The input that the row is based on. * @return {!Object} The new row, with the correct type and default sizing info. */ Blockly.BlockSvg.prototype.createRowForInput_ = function(input) { // Create new row. var row = []; if (input.type != Blockly.NEXT_STATEMENT) { row.type = Blockly.BlockSvg.INLINE; } else { row.type = input.type; } row.height = 0; // Default padding for a block: same as separators between fields/inputs. row.paddingStart = Blockly.BlockSvg.SEP_SPACE_X; row.paddingEnd = Blockly.BlockSvg.SEP_SPACE_X; return row; }; /** * Compute the preferred right edge of the block. * @param {number} curEdge The previously calculated right edge. * @param {boolean} hasStatement Whether this block has a statement input. * @return {number} The preferred right edge of the block. */ Blockly.BlockSvg.prototype.computeRightEdge_ = function(curEdge, hasStatement) { var edge = curEdge; if (this.previousConnection || this.nextConnection) { // Blocks with notches edge = Math.max(edge, Blockly.BlockSvg.MIN_BLOCK_X); } else if (this.outputConnection) { if (this.isShadow() && !Blockly.scratchBlocksUtils.isShadowArgumentReporter(this)) { // Single-fields edge = Math.max(edge, Blockly.BlockSvg.MIN_BLOCK_X_SHADOW_OUTPUT); } else { // Reporters edge = Math.max(edge, Blockly.BlockSvg.MIN_BLOCK_X_OUTPUT); } } if (hasStatement) { // Statement blocks (C- or E- shaped) have a longer minimum width. edge = Math.max(edge, Blockly.BlockSvg.MIN_BLOCK_X_WITH_STATEMENT); } // Ensure insertion markers are at least insertionMarkerMinWidth_ wide. if (this.insertionMarkerMinWidth_ > 0) { edge = Math.max(edge, this.insertionMarkerMinWidth_); } return edge; }; /** * For a block with output, * determine start and end padding, based on connected inputs. * Padding will depend on the shape of the output, the shape of the input, * and possibly the size of the input. * @param {!Array.<!Array.<!Object>>} inputRows Partially calculated rows. */ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) { // Only apply to blocks with outputs and not single fields (shadows). if (!this.getOutputShape() || !this.outputConnection || (this.isShadow() && !Blockly.scratchBlocksUtils.isShadowArgumentReporter(this))) { return; } // Blocks with outputs must have single row to be padded. if (inputRows.length > 1) { return; } var row = inputRows[0]; var shape = this.getOutputShape(); // Reset any padding: it's about to be set. row.paddingStart = 0; row.paddingEnd = 0; // Start row padding: based on first input or first field. var firstInput = row[0]; var firstField = firstInput.fieldRow[0]; var otherShape; // In checking the left/start side, a field takes precedence over any input. // That's because a field will be rendered before any value input. if (firstField) { otherShape = 0; // Field comes first in the row. } else { // Value input comes first in the row. var inputConnection = firstInput.connection; if (!inputConnection.targetConnection) { // Not connected: use the drawn shape. otherShape = inputConnection.getOutputShape(); } else { // Connected: use the connected block's output shape. otherShape = inputConnection.targetConnection.getSourceBlock().getOutputShape(); } // Special case for hexagonal output: if the connection is larger height // than a standard reporter, add some start padding. // https://github.com/LLK/scratch-blocks/issues/376 if (shape == Blockly.OUTPUT_SHAPE_HEXAGONAL && otherShape != Blockly.OUTPUT_SHAPE_HEXAGONAL) { var deltaHeight = firstInput.renderHeight - Blockly.BlockSvg.MIN_BLOCK_Y_REPORTER; // One grid unit per level of nesting. row.paddingStart += deltaHeight / 2; } } row.paddingStart += Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING[shape][otherShape]; // End row padding: based on last input or last field. var lastInput = row[row.length - 1]; // In checking the right/end side, any value input takes precedence over any field. // That's because fields are rendered before inputs...the last item // in the row will be an input, if one exists. if (lastInput.connection) { // Value input last in the row. var inputConnection = lastInput.connection; if (!inputConnection.targetConnection) { // Not connected: use the drawn shape. otherShape = inputConnection.getOutputShape(); } else { // Connected: use the connected block's output shape. otherShape = inputConnection.targetConnection.getSourceBlock().getOutputShape(); } // Special case for hexagonal output: if the connection is larger height // than a standard reporter, add some end padding. // https://github.com/LLK/scratch-blocks/issues/376 if (shape == Blockly.OUTPUT_SHAPE_HEXAGONAL && otherShape != Blockly.OUTPUT_SHAPE_HEXAGONAL) { var deltaHeight = lastInput.renderHeight - Blockly.BlockSvg.MIN_BLOCK_Y_REPORTER; // One grid unit per level of nesting. row.paddingEnd += deltaHeight / 2; } } else { // No input in this row - mark as field. otherShape = 0; } row.paddingEnd += Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING[shape][otherShape]; }; /** * Draw the path of the block. * Move the fields to the correct locations. * @param {number} iconWidth Offset of first row due to icons. * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each * containing position information. * @private */ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { this.startHat_ = false; // Should the top left corners be rounded or square? // Currently, it is squared only if it's a hat. this.squareTopLeftCorner_ = false; if (!this.outputConnection && !this.previousConnection) { // No output or previous connection. this.squareTopLeftCorner_ = true; this.startHat_ = true; inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); } // Amount of space to skip drawing the top and bottom, // to make room for the left and right to draw shapes (curves or angles). this.edgeShapeWidth_ = 0; this.edgeShape_ = null; if (this.outputConnection) { // Width of the curve/pointy-curve var shape = this.getOutputShape(); if (shape === Blockly.OUTPUT_SHAPE_HEXAGONAL || shape === Blockly.OUTPUT_SHAPE_ROUND) { this.edgeShapeWidth_ = inputRows.bottomEdge / 2; this.edgeShape_ = shape; this.squareTopLeftCorner_ = true; } } // Assemble the block's path. var steps = []; this.renderDrawTop_(steps, inputRows.rightEdge); var cursorY = this.renderDrawRight_(steps, inputRows, iconWidth); this.renderDrawBottom_(steps, cursorY); this.renderDrawLeft_(steps); var pathString = steps.join(' '); this.svgPath_.setAttribute('d', pathString); if (this.RTL) { // Mirror the block's path. // This is awesome. this.svgPath_.setAttribute('transform', 'scale(-1 1)'); } }; /** * Give the block an attribute 'data-shapes' that lists its shape[s], and an * attribute 'data-category' with its category. * @private */ Blockly.BlockSvg.prototype.renderClassify_ = function() { var shapes = []; if (this.outputConnection) { if (this.isShadow_) { shapes.push('argument'); } else { shapes.push('reporter'); } if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_HEXAGONAL) { shapes.push('boolean'); } else if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_ROUND) { shapes.push('round'); } } else { // count the number of statement inputs var inputList = this.inputList; var statementCount = 0; for (var i = 0, input; input = inputList[i]; i++) { if (input.connection && input.connection.type === Blockly.NEXT_STATEMENT) { statementCount++; } } if (statementCount) { shapes.push('c-block'); shapes.push('c-' + statementCount); } if (this.startHat_) { shapes.push('hat'); // c-block+hats are possible (e.x. reprter procedures) } else if (!statementCount) { shapes.push('stack'); //only call it "stack" if it's not a c-block } if (!this.nextConnection) { shapes.push('end'); } } this.svgGroup_.setAttribute('data-shapes', shapes.join(' ')); if (this.getCategory()) { this.svgGroup_.setAttribute('data-category', this.getCategory()); } }; /** * Render the top edge of the block. * @param {!Array.<string>} steps Path of block outline. * @param {number} rightEdge Minimum width of block. * @private */ Blockly.BlockSvg.prototype.renderDrawTop_ = function(steps, rightEdge) { if (this.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) { steps.push('m 0, 0'); steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT); } else { // Position the cursor at the top-left starting point. if (this.squareTopLeftCorner_) { steps.push('m 0,0'); if (this.startHat_) { steps.push(Blockly.BlockSvg.START_HAT_PATH); } // Skip space for the output shape if (this.edgeShapeWidth_) { steps.push('m ' + this.edgeShapeWidth_ + ',0'); } } else { steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START); // Top-left rounded corner. steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER); } // Top edge. if (this.previousConnection) { // Space before the notch steps.push('H', Blockly.BlockSvg.NOTCH_START_PADDING); steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT); // Create previous block connection. var connectionX = (this.RTL ? -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH); this.previousConnection.setOffsetInBlock(connectionX, 0); } } this.width = rightEdge; }; /** * Render the right edge of the block. * @param {!Array.<string>} steps Path of block outline. * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each * containing position information. * @param {number} iconWidth Offset of first row due to icons. * @return {number} Height of block. * @private */ Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, inputRows, iconWidth) { var cursorX = 0; var cursorY = 0; var connectionX, connectionY; for (var y = 0, row; row = inputRows[y]; y++) { cursorX = row.paddingStart; if (y == 0) { cursorX += this.RTL ? -iconWidth : iconWidth; } if (row.type == Blockly.BlockSvg.INLINE) { // Inline inputs. for (var x = 0, input; input = row[x]; x++) { // Align fields vertically within the row. // Moves the field to half of the row's height. // In renderFields_, the field is further centered // by its own rendered height. var fieldY = cursorY + row.height / 2; var fieldX = Blockly.BlockSvg.getAlignedCursor_(cursorX, input, inputRows.rightEdge); cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY); if (input.type == Blockly.INPUT_VALUE) { // Create inline input connection. // In blocks with a notch, inputs should be bumped to a min X, // to avoid overlapping with the notch. if (this.previousConnection) { cursorX = Math.max(cursorX, Blockly.BlockSvg.INPUT_AND_FIELD_MIN_X); } connectionX = this.RTL ? -cursorX : cursorX; // Attempt to center the connection vertically. var connectionYOffset = row.height / 2; connectionY = cursorY + connectionYOffset; input.connection.setOffsetInBlock(connectionX, connectionY); this.renderInputShape_(input, cursorX, cursorY + connectionYOffset); cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; } } // Remove final separator and replace it with right-padding. cursorX -= Blockly.BlockSvg.SEP_SPACE_X; cursorX += row.paddingEnd; // Update right edge for all inputs, such that all rows // stretch to be at least the size of all previous rows. inputRows.rightEdge = Math.max(cursorX, inputRows.rightEdge); // Move to the right edge cursorX = Math.max(cursorX, inputRows.rightEdge); this.width = Math.max(this.width, cursorX); if (!this.edgeShape_) { // Include corner radius in drawing the horizontal line. steps.push('H', cursorX - Blockly.BlockSvg.CORNER_RADIUS - this.edgeShapeWidth_); steps.push(Blockly.BlockSvg.TOP_RIGHT_CORNER); } else { // Don't include corner radius - no corner (edge shape drawn). steps.push('H', cursorX - this.edgeShapeWidth_); } // Subtract CORNER_RADIUS * 2 to account for the top right corner // and also the bottom right corner. Only move vertically the non-corner length. if (!this.edgeShape_) { steps.push('v', row.height - Blockly.BlockSvg.CORNER_RADIUS * 2); } } else if (row.type == Blockly.NEXT_STATEMENT) { // Nested statement. var input = row[0]; var fieldX = cursorX; // Align fields vertically within the row. // In renderFields_, the field is further centered by its own height. var fieldY = cursorY; fieldY += Blockly.BlockSvg.MIN_STATEMENT_INPUT_HEIGHT; this.renderFields_(input.fieldRow, fieldX, fieldY); // Move to the start of the notch. cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH; if (this.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) { this.renderDefineBlock_(steps, inputRows, input, row, cursorY); } else { Blockly.BlockSvg.drawStatementInputFromTopRight_(steps, cursorX, inputRows.rightEdge, row); } // Create statement connection. connectionX = this.RTL ? -cursorX : cursorX; input.connection.setOffsetInBlock(connectionX, cursorY); if (input.connection.isConnected()) { this.width = Math.max(this.width, inputRows.statementEdge + input.connection.targetBlock().getHeightWidth().width); } if (this.type != Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE && (y == inputRows.length - 1 || inputRows[y + 1].type == Blockly.NEXT_STATEMENT)) { // If the final input is a statement stack, add a small row underneath. // Consecutive statement stacks are also separated by a small divider. steps.push(Blockly.BlockSvg.TOP_RIGHT_CORNER); steps.push('v', Blockly.BlockSvg.EXTRA_STATEMENT_ROW_Y - 2 * Blockly.BlockSvg.CORNER_RADIUS); cursorY += Blockly.BlockSvg.EXTRA_STATEMENT_ROW_Y; } } cursorY += row.height; } this.drawEdgeShapeRight_(steps); if (!inputRows.length) { cursorY = Blockly.BlockSvg.MIN_BLOCK_Y; steps.push('V', cursorY); } return cursorY; }; /** * Render the input shapes. * If there's a connected block, hide the input shape. * Otherwise, draw and set the position of the input shape. * @param {!Blockly.Input} input Input to be rendered. * @param {Number} x X offset of input. * @param {Number} y Y offset of input. */ Blockly.BlockSvg.prototype.renderInputShape_ = function(input, x, y) { var inputShape = input.outlinePath; if (!inputShape) { // No input shape for this input - e.g., the block is an insertion marker. return; } // Input shapes are only visibly rendered on non-connected slots. if (input.connection.targetConnection) { inputShape.setAttribute('style', 'visibility: hidden'); } else { var inputShapeX = 0, inputShapeY = 0; var inputShapeInfo = Blockly.BlockSvg.getInputShapeInfo_(input.connection.getOutputShape()); if (this.RTL) { inputShapeX = -x - inputShapeInfo.width; } else { inputShapeX = x; } inputShapeY = y - (Blockly.BlockSvg.INPUT_SHAPE_HEIGHT / 2); inputShape.setAttribute('d', inputShapeInfo.path); inputShape.setAttribute('transform', 'translate(' + inputShapeX + ',' + inputShapeY + ')'); inputShape.setAttribute('data-argument-type', inputShapeInfo.argType); inputShape.setAttribute('style', 'visibility: visible'); } }; /** * Render the bottom edge of the block. * @param {!Array.<string>} steps Path of block outline. * @param {number} cursorY Height of block. * @private */ Blockly.BlockSvg.prototype.renderDrawBottom_ = function(steps, cursorY) { this.height = cursorY; if (!this.edgeShape_) { steps.push(Blockly.BlockSvg.BOTTOM_RIGHT_CORNER); } if (this.nextConnection) { // Move to the right-side of the notch. var notchStart = ( Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.NOTCH_START_PADDING + Blockly.BlockSvg.CORNER_RADIUS ); steps.push('H', notchStart, ' '); steps.push(Blockly.BlockSvg.NOTCH_PATH_RIGHT); // Create next block connection. var connectionX = this.RTL ? -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH; this.nextConnection.setOffsetInBlock(connectionX, cursorY); // Include height of notch in block height. this.height += Blockly.BlockSvg.NOTCH_HEIGHT; } // Bottom horizontal line if (!this.edgeShape_) { steps.push('H', Blockly.BlockSvg.CORNER_RADIUS); // Bottom left corner steps.push(Blockly.BlockSvg.BOTTOM_LEFT_CORNER); } else { steps.push('H', this.edgeShapeWidth_); } }; /** * Render the left edge of the block. * @param {!Array.<string>} steps Path of block outline. * @param {number} cursorY Height of block. * @private */ Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps) { if (this.outputConnection) { // Scratch-style reporters have output connection y at half block height. this.outputConnection.setOffsetInBlock(0, this.height / 2); } if (this.edgeShape_) { // Draw the left-side edge shape. if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_ROUND) { // Draw a rounded arc. steps.push('a ' + this.edgeShapeWidth_ + ' ' + this.edgeShapeWidth_ + ' 0 0 1 0 -' + this.edgeShapeWidth_ * 2); } else if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_HEXAGONAL) { // Draw a half-hexagon. steps.push('l ' + -this.edgeShapeWidth_ + ' ' + -this.edgeShapeWidth_ + ' l ' + this.edgeShapeWidth_ + ' ' + -this.edgeShapeWidth_); } } steps.push('z'); }; /** * Draw the edge shape (rounded or hexagonal) on the right side of a block with * an output. * @param {!Array.<string>} steps Path of block outline. * @private */ Blockly.BlockSvg.prototype.drawEdgeShapeRight_ = function(steps) { if (this.edgeShape_) { // Draw the right-side edge shape. if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_ROUND) { // Draw a rounded arc. steps.push('a ' + this.edgeShapeWidth_ + ' ' + this.edgeShapeWidth_ + ' 0 0 1 0 ' + this.edgeShapeWidth_ * 2); } else if (this.edgeShape_ === Blockly.OUTPUT_SHAPE_HEXAGONAL) { // Draw an half-hexagon. steps.push('l ' + this.edgeShapeWidth_ + ' ' + this.edgeShapeWidth_ + ' l ' + -this.edgeShapeWidth_ + ' ' + this.edgeShapeWidth_); } } }; /** * Position an new block correctly, so that it doesn't move the existing block * when connected to it. * @param {!Blockly.Block} newBlock The block to position - either the first * block in a dragged stack or an insertion marker. * @param {!Blockly.Connection} newConnection The connection on the new block's * stack - either a connection on newBlock, or the last NEXT_STATEMENT * connection on the stack if the stack's being dropped before another * block. * @param {!Blockly.Connection} existingConnection The connection on the * existing block, which newBlock should line up with. */ Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, existingConnection) { // We only need to position the new block if it's before the existing one, // otherwise its position is set by the previous block. if (newConnection.type == Blockly.NEXT_STATEMENT) { var dx = existingConnection.x_ - newConnection.x_; var dy = existingConnection.y_ - newConnection.y_; newBlock.moveBy(dx, dy); } }; /** * Draw the outline of a statement input, starting at the top right corner. * @param {!Array.<string>} steps Path of block outline. * @param {number} cursorX The x position of the start of the notch at the top * of the input. * @param {number} rightEdge The far right edge of the block, which determines * how wide the statement input is. * @param {!Array.<!Object>} row An object containing information about the * current row, including its height and whether it should have a notch at * the bottom. * @private */ Blockly.BlockSvg.drawStatementInputFromTopRight_ = function(steps, cursorX, rightEdge, row) { Blockly.BlockSvg.drawStatementInputTop_(steps, cursorX); steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); Blockly.BlockSvg.drawStatementInputBottom_(steps, rightEdge, row); }; /** * Draw the top of the outline of a statement input, starting at the top right * corner. * @param {!Array.<string>} steps Path of block outline. * @param {number} cursorX The x position of the start of the notch at the top * of the input. * @private */ Blockly.BlockSvg.drawStatementInputTop_ = function(steps, cursorX) { steps.push(Blockly.BlockSvg.BOTTOM_RIGHT_CORNER); steps.push('H', cursorX + Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE + 2 * Blockly.BlockSvg.CORNER_RADIUS); steps.push(Blockly.BlockSvg.NOTCH_PATH_RIGHT); steps.push('h', '-' + Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE); steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER); }; /** * Draw the bottom of the outline of a statement input, starting at the inner * left corner. * @param {!Array.<string>} steps Path of block outline. * @param {number} rightEdge The far right edge of the block, which determines * how wide the statement input is. * @param {!Array.<!Object>} row An object containing information about the * current row, including its height and whether it should have a notch at * the bottom. * @private */ Blockly.BlockSvg.drawStatementInputBottom_ = function(steps, rightEdge, row) { steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER); if (row.statementNotchAtBottom) { steps.push('h ', Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE); steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT); } steps.push('H', rightEdge - Blockly.BlockSvg.CORNER_RADIUS); }; /** * Render part of the hat and the right side of the define block to fully wrap * the connected statement block. * Scratch-specific. * @param {!Array.<string>} steps Path of block outline. * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each * containing position information. * @param {!Blockly.Input} input The input that is currently being rendered. * @param {!Array.<!Object>} row An object containing information about the * current row, including its height and whether it should have a notch at * the bottom. * @param {number} cursorY The y position of the start of this row. Used to * position the following dummy input's fields. * @private */ Blockly.BlockSvg.prototype.renderDefineBlock_ = function(steps, inputRows, input, row, cursorY) { // Following text shows up as a dummy input after the statement input, which // we are forcing to stay inline with the statement input instead of letting // it drop to a new line. var hasFollowingText = row.length == 2; // Figure out where the right side of the block is. var rightSide = inputRows.rightEdge; if (input.connection && input.connection.targetBlock()) { rightSide = inputRows.statementEdge + input.connection.targetBlock().getHeightWidth().width + Blockly.BlockSvg.DEFINE_BLOCK_PADDING_RIGHT; } else { // Handles the case where block is being rendered as an insertion marker rightSide = Math.max(Blockly.BlockSvg.MIN_BLOCK_X_WITH_STATEMENT, rightSide) + Blockly.BlockSvg.DEFINE_BLOCK_PADDING_RIGHT; } rightSide -= Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; if (hasFollowingText) { var followingTextInput = row[1]; var fieldStart = rightSide + 3 * Blockly.BlockSvg.SEP_SPACE_X; rightSide += followingTextInput.fieldRow[0].getSize().width; rightSide += 2 * Blockly.BlockSvg.SEP_SPACE_X; // Align fields vertically within the row. // In renderFields_, the field is further centered by its own height. // The dummy input's fields did not get laid out normally because we're // forcing them to stay inline with a statement input. var fieldY = cursorY; fieldY += Blockly.BlockSvg.MIN_STATEMENT_INPUT_HEIGHT; this.renderFields_(followingTextInput.fieldRow, fieldStart, fieldY); } // Draw the top and the right corner of the hat. steps.push('H', rightSide); steps.push(Blockly.BlockSvg.TOP_RIGHT_CORNER_DEFINE_HAT); row.height += 3 * Blockly.BlockSvg.GRID_UNIT; // Draw the right side of the block around the statement input. steps.push('v', row.height); // row.height will be used to update the cursor in the calling function. row.height += Blockly.BlockSvg.GRID_UNIT; }; /** * Get some information about the input shape to draw, based on the type of the * connection. * @param {number} shape An enum representing the shape of the connection we're * drawing around. * @return {!Object} An object containing an SVG path, a string representation * of the argument type, and a width. * @private */ Blockly.BlockSvg.getInputShapeInfo_ = function(shape) { var inputShapePath = null; var inputShapeArgType = null; var inputShapeWidth = 0; switch (shape) { case Blockly.OUTPUT_SHAPE_HEXAGONAL: inputShapePath = Blockly.BlockSvg.INPUT_SHAPE_HEXAGONAL; inputShapeWidth = Blockly.BlockSvg.INPUT_SHAPE_HEXAGONAL_WIDTH; inputShapeArgType = 'boolean'; break; case Blockly.OUTPUT_SHAPE_ROUND: inputShapePath = Blockly.BlockSvg.INPUT_SHAPE_ROUND; inputShapeWidth = Blockly.BlockSvg.INPUT_SHAPE_ROUND_WIDTH; inputShapeArgType = 'round'; break; case Blockly.OUTPUT_SHAPE_SQUARE: default: // If the input connection is not connected, draw a hole shape. inputShapePath = Blockly.BlockSvg.INPUT_SHAPE_SQUARE; inputShapeWidth = Blockly.BlockSvg.INPUT_SHAPE_SQUARE_WIDTH; inputShapeArgType = 'square'; break; } return { path: inputShapePath, argType: inputShapeArgType, width: inputShapeWidth }; }; /** * Get the correct cursor position for the given input, based on alignment, * the total size of the block, and the size of the input. * @param {number} cursorX The minimum x value of the cursor. * @param {!Blockly.Input} input The input to align the fields for. * @param {number} rightEdge The maximum width of the block. Right-aligned * fields are positioned based on this number. * @return {number} The new cursor position. * @private */ Blockly.BlockSvg.getAlignedCursor_ = function(cursorX, input, rightEdge) { // Align inline field rows (left/right/centre). if (input.align === Blockly.ALIGN_RIGHT) { cursorX += rightEdge - input.fieldWidth - (2 * Blockly.BlockSvg.SEP_SPACE_X); } else if (input.align === Blockly.ALIGN_CENTRE) { cursorX = Math.max(cursorX, rightEdge / 2 - input.fieldWidth / 2); } return cursorX; }; /** * Update all of the connections on this block with the new locaitons calculated * in renderCompute, and move all of the connected blocks based on the new * connection locations. * @private */ Blockly.BlockSvg.prototype.renderMoveConnections_ = function() { var blockTL = this.getRelativeToSurfaceXY(); // Don't tighten previous or output connections because they are inferior. if (this.previousConnection) { this.previousConnection.moveToOffset(blockTL); } if (this.outputConnection) { this.outputConnection.moveToOffset(blockTL); } for (var i = 0; i < this.inputList.length; i++) { var conn = this.inputList[i].connection; if (conn) { conn.moveToOffset(blockTL); if (conn.isConnected()) { conn.tighten_(); } } } if (this.nextConnection) { this.nextConnection.moveToOffset(blockTL); if (this.nextConnection.isConnected()) { this.nextConnection.tighten_(); } } };