/** * @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'); goog.require('goog.userAgent'); /** * Class for a block's SVG representation. * @param {!Blockly.Block} block The underlying block object. * @constructor */ Blockly.BlockSvg = function(block) { this.block_ = block; // Create core elements for the block. this.svgGroup_ = Blockly.createSvgElement('g', {}, null); this.svgPathDark_ = Blockly.createSvgElement('path', {'class': 'blocklyPathDark', 'transform': 'translate(1, 1)'}, this.svgGroup_); this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'}, this.svgGroup_); this.svgPathLight_ = Blockly.createSvgElement('path', {'class': 'blocklyPathLight'}, this.svgGroup_); this.svgPath_.tooltip = this.block_; Blockly.Tooltip.bindMouseEvents(this.svgPath_); this.updateMovable(); }; /** * Height of this block, not including any statement blocks above or below. */ Blockly.BlockSvg.prototype.height = 0; /** * Width of this block, including any connected value blocks. */ Blockly.BlockSvg.prototype.width = 0; /** * Constant for identifying rows that are to be rendered inline. * Don't collide with Blockly.INPUT_VALUE and friends. * @const */ Blockly.BlockSvg.INLINE = -1; /** * Initialize the SVG representation with any block attributes which have * already been defined. */ Blockly.BlockSvg.prototype.init = function() { var block = this.block_; this.updateColour(); for (var x = 0, input; input = block.inputList[x]; x++) { input.init(); } if (block.mutator) { block.mutator.createIcon(); } }; /** * Add or remove the UI indicating if this block is movable or not. */ Blockly.BlockSvg.prototype.updateMovable = function() { if (this.block_.isMovable()) { Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); } else { Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); } }; /** * Get the root SVG element. * @return {!Element} The root SVG element. */ Blockly.BlockSvg.prototype.getRootElement = function() { return this.svgGroup_; }; // UI constants for rendering blocks. /** * Horizontal space between elements. * @const */ Blockly.BlockSvg.SEP_SPACE_X = 10; /** * Vertical space between elements. * @const */ Blockly.BlockSvg.SEP_SPACE_Y = 10; /** * Vertical padding around inline elements. * @const */ Blockly.BlockSvg.INLINE_PADDING_Y = 5; /** * Minimum height of a block. * @const */ Blockly.BlockSvg.MIN_BLOCK_Y = 25; /** * Height of horizontal puzzle tab. * @const */ Blockly.BlockSvg.TAB_HEIGHT = 20; /** * Width of horizontal puzzle tab. * @const */ Blockly.BlockSvg.TAB_WIDTH = 8; /** * Width of vertical tab (inc left margin). * @const */ Blockly.BlockSvg.NOTCH_WIDTH = 30; /** * Rounded corner radius. * @const */ Blockly.BlockSvg.CORNER_RADIUS = 8; /** * Minimum height of field rows. * @const */ Blockly.BlockSvg.FIELD_HEIGHT = 18; /** * Distance from shape edge to intersect with a curved corner at 45 degrees. * Applies to highlighting on around the inside of a curve. * @const */ Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) * (Blockly.BlockSvg.CORNER_RADIUS - 1) + 1; /** * Distance from shape edge to intersect with a curved corner at 45 degrees. * Applies to highlighting on around the outside of a curve. * @const */ Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) * (Blockly.BlockSvg.CORNER_RADIUS + 1) - 1; /** * SVG path for drawing next/previous notch from left to right. * @const */ Blockly.BlockSvg.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4'; /** * SVG path for drawing next/previous notch from left to right with * highlighting. * @const */ Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT = 'l 6.5,4 2,0 6.5,-4'; /** * SVG path for drawing next/previous notch from right to left. * @const */ Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4'; /** * SVG path for drawing jagged teeth at the end of collapsed blocks. * @const */ Blockly.BlockSvg.JAGGED_TEETH = 'l 8,0 0,4 8,4 -16,8 8,4'; /** * Height of SVG path for jagged teeth at the end of collapsed blocks. * @const */ Blockly.BlockSvg.JAGGED_TEETH_HEIGHT = 20; /** * Width of SVG path for jagged teeth at the end of collapsed blocks. * @const */ Blockly.BlockSvg.JAGGED_TEETH_WIDTH = 15; /** * SVG path for drawing a horizontal puzzle tab from top to bottom. * @const */ Blockly.BlockSvg.TAB_PATH_DOWN = 'v 5 c 0,10 -' + Blockly.BlockSvg.TAB_WIDTH + ',-8 -' + Blockly.BlockSvg.TAB_WIDTH + ',7.5 s ' + Blockly.BlockSvg.TAB_WIDTH + ',-2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',7.5'; /** * SVG path for drawing a horizontal puzzle tab from top to bottom with * highlighting from the upper-right. * @const */ Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'v 6.5 m -' + (Blockly.BlockSvg.TAB_WIDTH * 0.98) + ',2.5 q -' + (Blockly.BlockSvg.TAB_WIDTH * .05) + ',10 ' + (Blockly.BlockSvg.TAB_WIDTH * .27) + ',10 m ' + (Blockly.BlockSvg.TAB_WIDTH * .71) + ',-2.5 v 1.5'; /** * SVG start point for drawing the top-left corner. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_START = 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS; /** * SVG start point for drawing the top-left corner's highlight in RTL. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL = 'm ' + Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + Blockly.BlockSvg.DISTANCE_45_INSIDE; /** * SVG start point for drawing the top-left corner's highlight in LTR. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR = 'm 1,' + (Blockly.BlockSvg.CORNER_RADIUS - 1); /** * 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 highlight on the rounded top-left corner. * @const */ Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT = 'A ' + (Blockly.BlockSvg.CORNER_RADIUS - 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS - 1) + ' 0 0,1 ' + Blockly.BlockSvg.CORNER_RADIUS + ',1'; /** * 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 */ Blockly.BlockSvg.INNER_TOP_LEFT_CORNER = Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -' + (Blockly.BlockSvg.NOTCH_WIDTH - 15 - Blockly.BlockSvg.CORNER_RADIUS) + ' 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 drawing highlight on the top-left corner of a statement * input in RTL. * @const */ Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL = 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' + (-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS - Blockly.BlockSvg.DISTANCE_45_OUTSIDE); /** * SVG path for drawing highlight on the bottom-left corner of a statement * input in RTL. * @const */ Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL = 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS + 1); /** * SVG path for drawing highlight on the bottom-left corner of a statement * input in LTR. * @const */ Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR = 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' + (Blockly.BlockSvg.CORNER_RADIUS - Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + (Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 1); /** * Dispose of this SVG block. */ Blockly.BlockSvg.prototype.dispose = function() { goog.dom.removeNode(this.svgGroup_); // Sever JavaScript to DOM connections. this.svgGroup_ = null; this.svgPath_ = null; this.svgPathLight_ = null; this.svgPathDark_ = null; // Break circular references. this.block_ = null; }; /** * Play some UI effects (sound, animation) when disposing of a block. */ Blockly.BlockSvg.prototype.disposeUiEffect = function() { Blockly.playAudio('delete'); var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_)); // Deeply clone the current block. var clone = this.svgGroup_.cloneNode(true); clone.translateX_ = xy.x; clone.translateY_ = xy.y; clone.setAttribute('transform', 'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')'); Blockly.svg.appendChild(clone); clone.bBox_ = clone.getBBox(); // Start the animation. clone.startDate_ = new Date(); Blockly.BlockSvg.disposeUiStep_(clone); }; /** * Animate a cloned block and eventually dispose of it. * @param {!Element} clone SVG element to animate and dispose of. * @private */ Blockly.BlockSvg.disposeUiStep_ = function(clone) { var ms = (new Date()) - clone.startDate_; var percent = ms / 150; if (percent > 1) { goog.dom.removeNode(clone); } else { var x = clone.translateX_ + (Blockly.RTL ? -1 : 1) * clone.bBox_.width / 2 * percent; var y = clone.translateY_ + clone.bBox_.height * percent; var translate = x + ', ' + y; var scale = 1 - percent; clone.setAttribute('transform', 'translate(' + translate + ')' + ' scale(' + scale + ')'); var closure = function() { Blockly.BlockSvg.disposeUiStep_(clone); }; window.setTimeout(closure, 10); } }; /** * Play some UI effects (sound, ripple) after a connection has been established. */ Blockly.BlockSvg.prototype.connectionUiEffect = function() { Blockly.playAudio('click'); // Determine the absolute coordinates of the inferior block. var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_)); // Offset the coordinates based on the two connection types. if (this.block_.outputConnection) { xy.x += Blockly.RTL ? 3 : -3; xy.y += 13; } else if (this.block_.previousConnection) { xy.x += Blockly.RTL ? -23 : 23; xy.y += 3; } var ripple = Blockly.createSvgElement('circle', {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none', 'stroke': '#888', 'stroke-width': 10}, Blockly.svg); // Start the animation. ripple.startDate_ = new Date(); Blockly.BlockSvg.connectionUiStep_(ripple); }; /** * Expand a ripple around a connection. * @param {!Element} ripple Element to animate. * @private */ Blockly.BlockSvg.connectionUiStep_ = function(ripple) { var ms = (new Date()) - ripple.startDate_; var percent = ms / 150; if (percent > 1) { goog.dom.removeNode(ripple); } else { ripple.setAttribute('r', percent * 25); ripple.style.opacity = 1 - percent; var closure = function() { Blockly.BlockSvg.connectionUiStep_(ripple); }; window.setTimeout(closure, 10); } }; /** * Change the colour of a block. */ Blockly.BlockSvg.prototype.updateColour = function() { if (this.block_.disabled) { // Disabled blocks don't have colour. return; } var hexColour = Blockly.makeColour(this.block_.getColour()); var rgb = goog.color.hexToRgb(hexColour); var rgbLight = goog.color.lighten(rgb, 0.3); var rgbDark = goog.color.darken(rgb, 0.4); this.svgPathLight_.setAttribute('stroke', goog.color.rgbArrayToHex(rgbLight)); this.svgPathDark_.setAttribute('fill', goog.color.rgbArrayToHex(rgbDark)); this.svgPath_.setAttribute('fill', hexColour); }; /** * Enable or disable a block. */ Blockly.BlockSvg.prototype.updateDisabled = function() { if (this.block_.disabled || this.block_.getInheritedDisabled()) { Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled'); this.svgPath_.setAttribute('fill', 'url(#blocklyDisabledPattern)'); } else { Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled'); this.updateColour(); } var children = this.block_.getChildren(); for (var x = 0, child; child = children[x]; x++) { child.svg_.updateDisabled(); } }; /** * Select this block. Highlight it visually. */ Blockly.BlockSvg.prototype.addSelect = function() { Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); // Move the selected block to the top of the stack. this.svgGroup_.parentNode.appendChild(this.svgGroup_); }; /** * Unselect this block. Remove its highlighting. */ Blockly.BlockSvg.prototype.removeSelect = function() { Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); }; /** * Adds the dragging class to this block. * Also disables the highlights/shadows to improve performance. */ Blockly.BlockSvg.prototype.addDragging = function() { Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); }; /** * Removes the dragging class from this block. */ Blockly.BlockSvg.prototype.removeDragging = function() { Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); }; /** * Render the block. * Lays out and reflows a block based on its contents and settings. */ Blockly.BlockSvg.prototype.render = function() { this.block_.rendered = true; var cursorX = Blockly.BlockSvg.SEP_SPACE_X; if (Blockly.RTL) { cursorX = -cursorX; } // Move the icons into position. var icons = this.block_.getIcons(); for (var x = 0; x < icons.length; x++) { cursorX = icons[x].renderIcon(cursorX); } cursorX += Blockly.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. var inputRows = this.renderCompute_(cursorX); this.renderDraw_(cursorX, inputRows); // Render all blocks above this one (propagate a reflow). var parentBlock = this.block_.getParent(); if (parentBlock) { parentBlock.render(); } else { // Top-most block. Fire an event to allow scrollbars to resize. Blockly.fireUiEvent(window, 'resize'); } }; /** * Render a list of fields starting at the specified location. * @param {!Array.} fieldList List of fields. * @param {number} cursorX X-coordinate to start the fields. * @param {number} cursorY Y-coordinate to start the fields. * @return {number} X-coordinate of the end of the field row (plus a gap). * @private */ Blockly.BlockSvg.prototype.renderFields_ = function(fieldList, cursorX, cursorY) { if (Blockly.RTL) { cursorX = -cursorX; } for (var t = 0, field; field = fieldList[t]; t++) { if (Blockly.RTL) { cursorX -= field.renderSep + field.renderWidth; field.getRootElement().setAttribute('transform', 'translate(' + cursorX + ', ' + cursorY + ')'); if (field.renderWidth) { cursorX -= Blockly.BlockSvg.SEP_SPACE_X; } } else { field.getRootElement().setAttribute('transform', 'translate(' + (cursorX + field.renderSep) + ', ' + cursorY + ')'); if (field.renderWidth) { cursorX += field.renderSep + field.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; } } } return Blockly.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.>} 2D array of objects, each containing * position information. * @private */ Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) { var inputList = this.block_.inputList; var inputRows = []; inputRows.rightEdge = iconWidth + Blockly.BlockSvg.SEP_SPACE_X * 2; if (this.block_.previousConnection || this.block_.nextConnection) { inputRows.rightEdge = Math.max(inputRows.rightEdge, Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.SEP_SPACE_X); } 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; var isInline = this.block_.inputsInline && !this.block_.isCollapsed(); for (var i = 0, input; input = inputList[i]; i++) { if (!input.isVisible()) { continue; } var row; if (!isInline || !lastType || lastType == Blockly.NEXT_STATEMENT || input.type == Blockly.NEXT_STATEMENT) { // Create new row. lastType = input.type; row = []; if (isInline && input.type != Blockly.NEXT_STATEMENT) { row.type = Blockly.BlockSvg.INLINE; } else { row.type = input.type; } row.height = 0; inputRows.push(row); } else { row = inputRows[inputRows.length - 1]; } row.push(input); // Compute minimum input size. input.renderHeight = Blockly.BlockSvg.MIN_BLOCK_Y; // The width is currently only needed for inline value inputs. if (isInline && input.type == Blockly.INPUT_VALUE) { input.renderWidth = Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X * 1.25; } else { input.renderWidth = 0; } // Expand input size if there is a connection. if (input.connection && input.connection.targetConnection) { var linkedBlock = input.connection.targetBlock(); var bBox = linkedBlock.getHeightWidth(); input.renderHeight = Math.max(input.renderHeight, bBox.height); input.renderWidth = Math.max(input.renderWidth, bBox.width); } if (i == inputList.length - 1) { // Last element should overhang slightly due to shadow. input.renderHeight--; } 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 += Blockly.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; 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); } } } // Make inline rows a bit thicker in order to enclose the values. for (var y = 0, row; row = inputRows[y]; y++) { row.thicker = false; if (row.type == Blockly.BlockSvg.INLINE) { for (var z = 0, input; input = row[z]; z++) { if (input.type == Blockly.INPUT_VALUE) { row.height += 2 * Blockly.BlockSvg.INLINE_PADDING_Y; row.thicker = true; break; } } } } // Compute the statement edge. // This is the width of a block where statements are nested. inputRows.statementEdge = 2 * Blockly.BlockSvg.SEP_SPACE_X + fieldStatementWidth; // Compute the preferred right edge. Inline blocks may extend beyond. // This is the width of the block where external inputs connect. if (hasStatement) { inputRows.rightEdge = Math.max(inputRows.rightEdge, inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH); } if (hasValue) { inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.TAB_WIDTH); } else if (hasDummy) { inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth + Blockly.BlockSvg.SEP_SPACE_X * 2); } inputRows.hasValue = hasValue; inputRows.hasStatement = hasStatement; inputRows.hasDummy = hasDummy; return inputRows; }; /** * 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.>} inputRows 2D array of objects, each * containing position information. * @private */ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { // Should the top and bottom left corners be rounded or square? if (this.block_.outputConnection) { this.squareTopLeftCorner_ = true; this.squareBottomLeftCorner_ = true; } else { this.squareTopLeftCorner_ = false; this.squareBottomLeftCorner_ = false; // If this block is in the middle of a stack, square the corners. if (this.block_.previousConnection) { var prevBlock = this.block_.previousConnection.targetBlock(); if (prevBlock && prevBlock.getNextBlock() == this.block_) { this.squareTopLeftCorner_ = true; } } var nextBlock = this.block_.getNextBlock(); if (nextBlock) { this.squareBottomLeftCorner_ = true; } } // Fetch the block's coordinates on the surface for use in anchoring // the connections. var connectionsXY = this.block_.getRelativeToSurfaceXY(); // Assemble the block's path. var steps = []; var inlineSteps = []; // The highlighting applies to edges facing the upper-left corner. // Since highlighting is a two-pixel wide border, it would normally overhang // the edge of the block by a pixel. So undersize all measurements by a pixel. var highlightSteps = []; var highlightInlineSteps = []; this.renderDrawTop_(steps, highlightSteps, connectionsXY, inputRows.rightEdge); var cursorY = this.renderDrawRight_(steps, highlightSteps, inlineSteps, highlightInlineSteps, connectionsXY, inputRows, iconWidth); this.renderDrawBottom_(steps, highlightSteps, connectionsXY, cursorY); this.renderDrawLeft_(steps, highlightSteps, connectionsXY, cursorY); var pathString = steps.join(' ') + '\n' + inlineSteps.join(' '); this.svgPath_.setAttribute('d', pathString); this.svgPathDark_.setAttribute('d', pathString); pathString = highlightSteps.join(' ') + '\n' + highlightInlineSteps.join(' '); this.svgPathLight_.setAttribute('d', pathString); if (Blockly.RTL) { // Mirror the block's path. this.svgPath_.setAttribute('transform', 'scale(-1 1)'); this.svgPathLight_.setAttribute('transform', 'scale(-1 1)'); this.svgPathDark_.setAttribute('transform', 'translate(1,1) scale(-1 1)'); } }; /** * Render the top edge of the block. * @param {!Array.} steps Path of block outline. * @param {!Array.} highlightSteps Path of block highlights. * @param {!Object} connectionsXY Location of block. * @param {number} rightEdge Minimum width of block. * @private */ Blockly.BlockSvg.prototype.renderDrawTop_ = function(steps, highlightSteps, connectionsXY, rightEdge) { // Position the cursor at the top-left starting point. if (this.squareTopLeftCorner_) { steps.push('m 0,0'); highlightSteps.push('m 1,1'); } else { steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START); highlightSteps.push(Blockly.RTL ? Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL : Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR); // Top-left rounded corner. steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER); highlightSteps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT); } // Top edge. if (this.block_.previousConnection) { steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); highlightSteps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15); steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT); highlightSteps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT); // Create previous block connection. var connectionX = connectionsXY.x + (Blockly.RTL ? -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH); var connectionY = connectionsXY.y; this.block_.previousConnection.moveTo(connectionX, connectionY); // This connection will be tightened when the parent renders. } steps.push('H', rightEdge); highlightSteps.push('H', rightEdge + (Blockly.RTL ? -1 : 0)); this.width = rightEdge; }; /** * Render the right edge of the block. * @param {!Array.} steps Path of block outline. * @param {!Array.} highlightSteps Path of block highlights. * @param {!Array.} inlineSteps Inline block outlines. * @param {!Array.} highlightInlineSteps Inline block highlights. * @param {!Object} connectionsXY Location of block. * @param {!Array.>} 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, highlightSteps, inlineSteps, highlightInlineSteps, connectionsXY, inputRows, iconWidth) { var cursorX; var cursorY = 0; var connectionX, connectionY; for (var y = 0, row; row = inputRows[y]; y++) { cursorX = Blockly.BlockSvg.SEP_SPACE_X; if (y == 0) { cursorX += Blockly.RTL ? -iconWidth : iconWidth; } highlightSteps.push('M', (inputRows.rightEdge - 1) + ',' + (cursorY + 1)); if (this.block_.isCollapsed()) { // Jagged right edge. var input = row[0]; var fieldX = cursorX; var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT; this.renderFields_(input.fieldRow, fieldX, fieldY); steps.push(Blockly.BlockSvg.JAGGED_TEETH); if (Blockly.RTL) { highlightSteps.push('l 8,0 0,3.8 7,3.2 m -14.5,9 l 8,4'); } else { highlightSteps.push('h 8'); } var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT; steps.push('v', remainder); if (Blockly.RTL) { highlightSteps.push('v', remainder - 2); } this.width += Blockly.BlockSvg.JAGGED_TEETH_WIDTH; } else if (row.type == Blockly.BlockSvg.INLINE) { // Inline inputs. for (var x = 0, input; input = row[x]; x++) { var fieldX = cursorX; var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT; if (row.thicker) { // Lower the field slightly. fieldY += Blockly.BlockSvg.INLINE_PADDING_Y; } // TODO: Align inline field rows (left/right/centre). cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY); if (input.type != Blockly.DUMMY_INPUT) { cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X; } if (input.type == Blockly.INPUT_VALUE) { inlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y)); inlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - input.renderWidth); inlineSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN); inlineSteps.push('v', input.renderHeight + 1 - Blockly.BlockSvg.TAB_HEIGHT); inlineSteps.push('h', input.renderWidth + 2 - Blockly.BlockSvg.TAB_WIDTH); inlineSteps.push('z'); if (Blockly.RTL) { // Highlight right edge, around back of tab, and bottom. highlightInlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X - 3 + Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1)); highlightInlineSteps.push( Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); highlightInlineSteps.push('v', input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 3); highlightInlineSteps.push('h', input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 1); } else { // Highlight right edge, bottom, and glint at bottom of tab. highlightInlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X + 1) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1)); highlightInlineSteps.push('v', input.renderHeight + 1); highlightInlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 - input.renderWidth); highlightInlineSteps.push('M', (cursorX - input.renderWidth - Blockly.BlockSvg.SEP_SPACE_X + 0.8) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + Blockly.BlockSvg.TAB_HEIGHT - 0.4)); highlightInlineSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.42) + ',-1.8'); } // Create inline input connection. if (Blockly.RTL) { connectionX = connectionsXY.x - cursorX - Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X + input.renderWidth + 1; } else { connectionX = connectionsXY.x + cursorX + Blockly.BlockSvg.TAB_WIDTH - Blockly.BlockSvg.SEP_SPACE_X - input.renderWidth - 1; } connectionY = connectionsXY.y + cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1; input.connection.moveTo(connectionX, connectionY); if (input.connection.targetConnection) { input.connection.tighten_(); } } } cursorX = Math.max(cursorX, inputRows.rightEdge); this.width = Math.max(this.width, cursorX); steps.push('H', cursorX); highlightSteps.push('H', cursorX + (Blockly.RTL ? -1 : 0)); steps.push('v', row.height); if (Blockly.RTL) { highlightSteps.push('v', row.height - 2); } } else if (row.type == Blockly.INPUT_VALUE) { // External input. var input = row[0]; var fieldX = cursorX; var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT; if (input.align != Blockly.ALIGN_LEFT) { var fieldRightX = inputRows.rightEdge - input.fieldWidth - Blockly.BlockSvg.TAB_WIDTH - 2 * Blockly.BlockSvg.SEP_SPACE_X; if (input.align == Blockly.ALIGN_RIGHT) { fieldX += fieldRightX; } else if (input.align == Blockly.ALIGN_CENTRE) { fieldX += (fieldRightX + fieldX) / 2; } } this.renderFields_(input.fieldRow, fieldX, fieldY); steps.push(Blockly.BlockSvg.TAB_PATH_DOWN); var v = row.height - Blockly.BlockSvg.TAB_HEIGHT; steps.push('v', v); if (Blockly.RTL) { // Highlight around back of tab. highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL); highlightSteps.push('v', v); } else { // Short highlight glint at bottom of tab. highlightSteps.push('M', (inputRows.rightEdge - 4.2) + ',' + (cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.4)); highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.42) + ',-1.8'); } // Create external input connection. connectionX = connectionsXY.x + (Blockly.RTL ? -inputRows.rightEdge - 1 : inputRows.rightEdge + 1); connectionY = connectionsXY.y + cursorY; input.connection.moveTo(connectionX, connectionY); if (input.connection.targetConnection) { input.connection.tighten_(); this.width = Math.max(this.width, inputRows.rightEdge + input.connection.targetBlock().getHeightWidth().width - Blockly.BlockSvg.TAB_WIDTH + 1); } } else if (row.type == Blockly.DUMMY_INPUT) { // External naked field. var input = row[0]; var fieldX = cursorX; var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT; if (input.align != Blockly.ALIGN_LEFT) { var fieldRightX = inputRows.rightEdge - input.fieldWidth - 2 * Blockly.BlockSvg.SEP_SPACE_X; if (inputRows.hasValue) { fieldRightX -= Blockly.BlockSvg.TAB_WIDTH; } if (input.align == Blockly.ALIGN_RIGHT) { fieldX += fieldRightX; } else if (input.align == Blockly.ALIGN_CENTRE) { fieldX += (fieldRightX + fieldX) / 2; } } this.renderFields_(input.fieldRow, fieldX, fieldY); steps.push('v', row.height); if (Blockly.RTL) { highlightSteps.push('v', row.height - 2); } } else if (row.type == Blockly.NEXT_STATEMENT) { // Nested statement. var input = row[0]; if (y == 0) { // If the first input is a statement stack, add a small row on top. steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y); if (Blockly.RTL) { highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); } cursorY += Blockly.BlockSvg.SEP_SPACE_Y; } var fieldX = cursorX; var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT; if (input.align != Blockly.ALIGN_LEFT) { var fieldRightX = inputRows.statementEdge - input.fieldWidth - 2 * Blockly.BlockSvg.SEP_SPACE_X; if (input.align == Blockly.ALIGN_RIGHT) { fieldX += fieldRightX; } else if (input.align == Blockly.ALIGN_CENTRE) { fieldX += (fieldRightX + fieldX) / 2; } } this.renderFields_(input.fieldRow, fieldX, fieldY); cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH; steps.push('H', cursorX); steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER); steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER); steps.push('H', inputRows.rightEdge); if (Blockly.RTL) { highlightSteps.push('M', (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); highlightSteps.push( Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL); highlightSteps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS); highlightSteps.push( Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL); highlightSteps.push('H', inputRows.rightEdge - 1); } else { highlightSteps.push('M', (cursorX - Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' + (cursorY + row.height - Blockly.BlockSvg.DISTANCE_45_OUTSIDE)); highlightSteps.push( Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR); highlightSteps.push('H', inputRows.rightEdge); } // Create statement connection. connectionX = connectionsXY.x + (Blockly.RTL ? -cursorX : cursorX); connectionY = connectionsXY.y + cursorY + 1; input.connection.moveTo(connectionX, connectionY); if (input.connection.targetConnection) { input.connection.tighten_(); this.width = Math.max(this.width, inputRows.statementEdge + input.connection.targetBlock().getHeightWidth().width); } if (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('v', Blockly.BlockSvg.SEP_SPACE_Y); if (Blockly.RTL) { highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1); } cursorY += Blockly.BlockSvg.SEP_SPACE_Y; } } cursorY += row.height; } if (!inputRows.length) { cursorY = Blockly.BlockSvg.MIN_BLOCK_Y; steps.push('V', cursorY); if (Blockly.RTL) { highlightSteps.push('V', cursorY - 1); } } return cursorY; }; /** * Render the bottom edge of the block. * @param {!Array.} steps Path of block outline. * @param {!Array.} highlightSteps Path of block highlights. * @param {!Object} connectionsXY Location of block. * @param {number} cursorY Height of block. * @private */ Blockly.BlockSvg.prototype.renderDrawBottom_ = function(steps, highlightSteps, connectionsXY, cursorY) { this.height = cursorY + 1; // Add one for the shadow. if (this.block_.nextConnection) { steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH + ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT); // Create next block connection. var connectionX; if (Blockly.RTL) { connectionX = connectionsXY.x - Blockly.BlockSvg.NOTCH_WIDTH; } else { connectionX = connectionsXY.x + Blockly.BlockSvg.NOTCH_WIDTH; } var connectionY = connectionsXY.y + cursorY + 1; this.block_.nextConnection.moveTo(connectionX, connectionY); if (this.block_.nextConnection.targetConnection) { this.block_.nextConnection.tighten_(); } this.height += 4; // Height of tab. } // Should the bottom-left corner be rounded or square? if (this.squareBottomLeftCorner_) { steps.push('H 0'); if (!Blockly.RTL) { highlightSteps.push('M', '1,' + cursorY); } } else { steps.push('H', Blockly.BlockSvg.CORNER_RADIUS); steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' + Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' + Blockly.BlockSvg.CORNER_RADIUS + ',-' + Blockly.BlockSvg.CORNER_RADIUS); if (!Blockly.RTL) { highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' + (cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE)); highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 1) + ',' + (Blockly.BlockSvg.CORNER_RADIUS - 1) + ' 0 0,1 ' + '1,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS)); } } }; /** * Render the left edge of the block. * @param {!Array.} steps Path of block outline. * @param {!Array.} highlightSteps Path of block highlights. * @param {!Object} connectionsXY Location of block. * @param {number} cursorY Height of block. * @private */ Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, highlightSteps, connectionsXY, cursorY) { if (this.block_.outputConnection) { // Create output connection. this.block_.outputConnection.moveTo(connectionsXY.x, connectionsXY.y); // This connection will be tightened when the parent renders. steps.push('V', Blockly.BlockSvg.TAB_HEIGHT); steps.push('c 0,-10 -' + Blockly.BlockSvg.TAB_WIDTH + ',8 -' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5 s ' + Blockly.BlockSvg.TAB_WIDTH + ',2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5'); if (Blockly.RTL) { highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.3) + ',8.9'); highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1'); } else { highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1); highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) + ',-1 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) + ',-5.5 0,-11'); highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) + ',1 V 1 H 2'); } this.width += Blockly.BlockSvg.TAB_WIDTH; } else if (!Blockly.RTL) { if (this.squareTopLeftCorner_) { highlightSteps.push('V', 1); } else { highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS); } } steps.push('z'); };