block_render_svg.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2016 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Methods for graphically rendering a block as SVG.
  22. * @author fenichel@google.com (Rachel Fenichel)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.BlockSvg.render');
  26. goog.require('Blockly.BlockSvg');
  27. // UI constants for rendering blocks.
  28. /**
  29. * Horizontal space between elements.
  30. * @const
  31. */
  32. Blockly.BlockSvg.SEP_SPACE_X = 10;
  33. /**
  34. * Vertical space between elements.
  35. * @const
  36. */
  37. Blockly.BlockSvg.SEP_SPACE_Y = 10;
  38. /**
  39. * Vertical padding around inline elements.
  40. * @const
  41. */
  42. Blockly.BlockSvg.INLINE_PADDING_Y = 5;
  43. /**
  44. * Minimum height of a block.
  45. * @const
  46. */
  47. Blockly.BlockSvg.MIN_BLOCK_Y = 25;
  48. /**
  49. * Height of horizontal puzzle tab.
  50. * @const
  51. */
  52. Blockly.BlockSvg.TAB_HEIGHT = 20;
  53. /**
  54. * Width of horizontal puzzle tab.
  55. * @const
  56. */
  57. Blockly.BlockSvg.TAB_WIDTH = 8;
  58. /**
  59. * Width of vertical tab (inc left margin).
  60. * @const
  61. */
  62. Blockly.BlockSvg.NOTCH_WIDTH = 30;
  63. /**
  64. * Rounded corner radius.
  65. * @const
  66. */
  67. Blockly.BlockSvg.CORNER_RADIUS = 8;
  68. /**
  69. * Do blocks with no previous or output connections have a 'hat' on top?
  70. * @const
  71. */
  72. Blockly.BlockSvg.START_HAT = false;
  73. /**
  74. * Height of the top hat.
  75. * @const
  76. */
  77. Blockly.BlockSvg.START_HAT_HEIGHT = 15;
  78. /**
  79. * Path of the top hat's curve.
  80. * @const
  81. */
  82. Blockly.BlockSvg.START_HAT_PATH = 'c 30,-' +
  83. Blockly.BlockSvg.START_HAT_HEIGHT + ' 70,-' +
  84. Blockly.BlockSvg.START_HAT_HEIGHT + ' 100,0';
  85. /**
  86. * Path of the top hat's curve's highlight in LTR.
  87. * @const
  88. */
  89. Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR =
  90. 'c 17.8,-9.2 45.3,-14.9 75,-8.7 M 100.5,0.5';
  91. /**
  92. * Path of the top hat's curve's highlight in RTL.
  93. * @const
  94. */
  95. Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL =
  96. 'm 25,-8.7 c 29.7,-6.2 57.2,-0.5 75,8.7';
  97. /**
  98. * Distance from shape edge to intersect with a curved corner at 45 degrees.
  99. * Applies to highlighting on around the inside of a curve.
  100. * @const
  101. */
  102. Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) *
  103. (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + 0.5;
  104. /**
  105. * Distance from shape edge to intersect with a curved corner at 45 degrees.
  106. * Applies to highlighting on around the outside of a curve.
  107. * @const
  108. */
  109. Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) *
  110. (Blockly.BlockSvg.CORNER_RADIUS + 0.5) - 0.5;
  111. /**
  112. * SVG path for drawing next/previous notch from left to right.
  113. * @const
  114. */
  115. Blockly.BlockSvg.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4';
  116. /**
  117. * SVG path for drawing next/previous notch from left to right with
  118. * highlighting.
  119. * @const
  120. */
  121. Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT = 'l 6,4 3,0 6,-4';
  122. /**
  123. * SVG path for drawing next/previous notch from right to left.
  124. * @const
  125. */
  126. Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4';
  127. /**
  128. * SVG path for drawing jagged teeth at the end of collapsed blocks.
  129. * @const
  130. */
  131. Blockly.BlockSvg.JAGGED_TEETH = 'l 8,0 0,4 8,4 -16,8 8,4';
  132. /**
  133. * Height of SVG path for jagged teeth at the end of collapsed blocks.
  134. * @const
  135. */
  136. Blockly.BlockSvg.JAGGED_TEETH_HEIGHT = 20;
  137. /**
  138. * Width of SVG path for jagged teeth at the end of collapsed blocks.
  139. * @const
  140. */
  141. Blockly.BlockSvg.JAGGED_TEETH_WIDTH = 15;
  142. /**
  143. * SVG path for drawing a horizontal puzzle tab from top to bottom.
  144. * @const
  145. */
  146. Blockly.BlockSvg.TAB_PATH_DOWN = 'v 5 c 0,10 -' + Blockly.BlockSvg.TAB_WIDTH +
  147. ',-8 -' + Blockly.BlockSvg.TAB_WIDTH + ',7.5 s ' +
  148. Blockly.BlockSvg.TAB_WIDTH + ',-2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',7.5';
  149. /**
  150. * SVG path for drawing a horizontal puzzle tab from top to bottom with
  151. * highlighting from the upper-right.
  152. * @const
  153. */
  154. Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'v 6.5 m -' +
  155. (Blockly.BlockSvg.TAB_WIDTH * 0.97) + ',3 q -' +
  156. (Blockly.BlockSvg.TAB_WIDTH * 0.05) + ',10 ' +
  157. (Blockly.BlockSvg.TAB_WIDTH * 0.3) + ',9.5 m ' +
  158. (Blockly.BlockSvg.TAB_WIDTH * 0.67) + ',-1.9 v 1.4';
  159. /**
  160. * SVG start point for drawing the top-left corner.
  161. * @const
  162. */
  163. Blockly.BlockSvg.TOP_LEFT_CORNER_START =
  164. 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS;
  165. /**
  166. * SVG start point for drawing the top-left corner's highlight in RTL.
  167. * @const
  168. */
  169. Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL =
  170. 'm ' + Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' +
  171. Blockly.BlockSvg.DISTANCE_45_INSIDE;
  172. /**
  173. * SVG start point for drawing the top-left corner's highlight in LTR.
  174. * @const
  175. */
  176. Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR =
  177. 'm 0.5,' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5);
  178. /**
  179. * SVG path for drawing the rounded top-left corner.
  180. * @const
  181. */
  182. Blockly.BlockSvg.TOP_LEFT_CORNER =
  183. 'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
  184. Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
  185. Blockly.BlockSvg.CORNER_RADIUS + ',0';
  186. /**
  187. * SVG path for drawing the highlight on the rounded top-left corner.
  188. * @const
  189. */
  190. Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT =
  191. 'A ' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' +
  192. (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' +
  193. Blockly.BlockSvg.CORNER_RADIUS + ',0.5';
  194. /**
  195. * SVG path for drawing the top-left corner of a statement input.
  196. * Includes the top notch, a horizontal space, and the rounded inside corner.
  197. * @const
  198. */
  199. Blockly.BlockSvg.INNER_TOP_LEFT_CORNER =
  200. Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -' +
  201. (Blockly.BlockSvg.NOTCH_WIDTH - 15 - Blockly.BlockSvg.CORNER_RADIUS) +
  202. ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
  203. Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' +
  204. Blockly.BlockSvg.CORNER_RADIUS + ',' +
  205. Blockly.BlockSvg.CORNER_RADIUS;
  206. /**
  207. * SVG path for drawing the bottom-left corner of a statement input.
  208. * Includes the rounded inside corner.
  209. * @const
  210. */
  211. Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER =
  212. 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
  213. Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
  214. Blockly.BlockSvg.CORNER_RADIUS + ',' +
  215. Blockly.BlockSvg.CORNER_RADIUS;
  216. /**
  217. * SVG path for drawing highlight on the top-left corner of a statement
  218. * input in RTL.
  219. * @const
  220. */
  221. Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL =
  222. 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
  223. Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
  224. (-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 0.5) + ',' +
  225. (Blockly.BlockSvg.CORNER_RADIUS -
  226. Blockly.BlockSvg.DISTANCE_45_OUTSIDE);
  227. /**
  228. * SVG path for drawing highlight on the bottom-left corner of a statement
  229. * input in RTL.
  230. * @const
  231. */
  232. Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL =
  233. 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' +
  234. (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' +
  235. (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' +
  236. (Blockly.BlockSvg.CORNER_RADIUS + 0.5);
  237. /**
  238. * SVG path for drawing highlight on the bottom-left corner of a statement
  239. * input in LTR.
  240. * @const
  241. */
  242. Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR =
  243. 'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' +
  244. (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' +
  245. (Blockly.BlockSvg.CORNER_RADIUS -
  246. Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' +
  247. (Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 0.5);
  248. /**
  249. * Render the block.
  250. * Lays out and reflows a block based on its contents and settings.
  251. * @param {boolean=} opt_bubble If false, just render this block.
  252. * If true, also render block's parent, grandparent, etc. Defaults to true.
  253. */
  254. Blockly.BlockSvg.prototype.render = function(opt_bubble) {
  255. Blockly.Field.startCache();
  256. this.rendered = true;
  257. var cursorX = Blockly.BlockSvg.SEP_SPACE_X;
  258. if (this.RTL) {
  259. cursorX = -cursorX;
  260. }
  261. // Move the icons into position.
  262. var icons = this.getIcons();
  263. for (var i = 0; i < icons.length; i++) {
  264. cursorX = icons[i].renderIcon(cursorX);
  265. }
  266. cursorX += this.RTL ?
  267. Blockly.BlockSvg.SEP_SPACE_X : -Blockly.BlockSvg.SEP_SPACE_X;
  268. // If there are no icons, cursorX will be 0, otherwise it will be the
  269. // width that the first label needs to move over by.
  270. var inputRows = this.renderCompute_(cursorX);
  271. this.renderDraw_(cursorX, inputRows);
  272. this.renderMoveConnections_();
  273. if (opt_bubble !== false) {
  274. // Render all blocks above this one (propagate a reflow).
  275. var parentBlock = this.getParent();
  276. if (parentBlock) {
  277. parentBlock.render(true);
  278. } else {
  279. // Top-most block. Fire an event to allow scrollbars to resize.
  280. this.workspace.resizeContents();
  281. }
  282. }
  283. Blockly.Field.stopCache();
  284. };
  285. /**
  286. * Render a list of fields starting at the specified location.
  287. * @param {!Array.<!Blockly.Field>} fieldList List of fields.
  288. * @param {number} cursorX X-coordinate to start the fields.
  289. * @param {number} cursorY Y-coordinate to start the fields.
  290. * @return {number} X-coordinate of the end of the field row (plus a gap).
  291. * @private
  292. */
  293. Blockly.BlockSvg.prototype.renderFields_ =
  294. function(fieldList, cursorX, cursorY) {
  295. /* eslint-disable indent */
  296. cursorY += Blockly.BlockSvg.INLINE_PADDING_Y;
  297. if (this.RTL) {
  298. cursorX = -cursorX;
  299. }
  300. for (var t = 0, field; field = fieldList[t]; t++) {
  301. var root = field.getSvgRoot();
  302. if (!root) {
  303. continue;
  304. }
  305. if (this.RTL) {
  306. cursorX -= field.renderSep + field.renderWidth;
  307. root.setAttribute('transform',
  308. 'translate(' + cursorX + ',' + cursorY + ')');
  309. if (field.renderWidth) {
  310. cursorX -= Blockly.BlockSvg.SEP_SPACE_X;
  311. }
  312. } else {
  313. root.setAttribute('transform',
  314. 'translate(' + (cursorX + field.renderSep) + ',' + cursorY + ')');
  315. if (field.renderWidth) {
  316. cursorX += field.renderSep + field.renderWidth +
  317. Blockly.BlockSvg.SEP_SPACE_X;
  318. }
  319. }
  320. }
  321. return this.RTL ? -cursorX : cursorX;
  322. }; /* eslint-enable indent */
  323. /**
  324. * Computes the height and widths for each row and field.
  325. * @param {number} iconWidth Offset of first row due to icons.
  326. * @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing
  327. * position information.
  328. * @private
  329. */
  330. Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) {
  331. var inputList = this.inputList;
  332. var inputRows = [];
  333. inputRows.rightEdge = iconWidth + Blockly.BlockSvg.SEP_SPACE_X * 2;
  334. if (this.previousConnection || this.nextConnection) {
  335. inputRows.rightEdge = Math.max(inputRows.rightEdge,
  336. Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.SEP_SPACE_X);
  337. }
  338. var fieldValueWidth = 0; // Width of longest external value field.
  339. var fieldStatementWidth = 0; // Width of longest statement field.
  340. var hasValue = false;
  341. var hasStatement = false;
  342. var hasDummy = false;
  343. var lastType = undefined;
  344. var isInline = this.getInputsInline() && !this.isCollapsed();
  345. for (var i = 0, input; input = inputList[i]; i++) {
  346. if (!input.isVisible()) {
  347. continue;
  348. }
  349. var row;
  350. if (!isInline || !lastType ||
  351. lastType == Blockly.NEXT_STATEMENT ||
  352. input.type == Blockly.NEXT_STATEMENT) {
  353. // Create new row.
  354. lastType = input.type;
  355. row = [];
  356. if (isInline && input.type != Blockly.NEXT_STATEMENT) {
  357. row.type = Blockly.BlockSvg.INLINE;
  358. } else {
  359. row.type = input.type;
  360. }
  361. row.height = 0;
  362. inputRows.push(row);
  363. } else {
  364. row = inputRows[inputRows.length - 1];
  365. }
  366. row.push(input);
  367. // Compute minimum input size.
  368. input.renderHeight = Blockly.BlockSvg.MIN_BLOCK_Y;
  369. // The width is currently only needed for inline value inputs.
  370. if (isInline && input.type == Blockly.INPUT_VALUE) {
  371. input.renderWidth = Blockly.BlockSvg.TAB_WIDTH +
  372. Blockly.BlockSvg.SEP_SPACE_X * 1.25;
  373. } else {
  374. input.renderWidth = 0;
  375. }
  376. // Expand input size if there is a connection.
  377. if (input.connection && input.connection.isConnected()) {
  378. var linkedBlock = input.connection.targetBlock();
  379. var bBox = linkedBlock.getHeightWidth();
  380. input.renderHeight = Math.max(input.renderHeight, bBox.height);
  381. input.renderWidth = Math.max(input.renderWidth, bBox.width);
  382. }
  383. // Blocks have a one pixel shadow that should sometimes overhang.
  384. if (!isInline && i == inputList.length - 1) {
  385. // Last value input should overhang.
  386. input.renderHeight--;
  387. } else if (!isInline && input.type == Blockly.INPUT_VALUE &&
  388. inputList[i + 1] && inputList[i + 1].type == Blockly.NEXT_STATEMENT) {
  389. // Value input above statement input should overhang.
  390. input.renderHeight--;
  391. }
  392. row.height = Math.max(row.height, input.renderHeight);
  393. input.fieldWidth = 0;
  394. if (inputRows.length == 1) {
  395. // The first row gets shifted to accommodate any icons.
  396. input.fieldWidth += this.RTL ? -iconWidth : iconWidth;
  397. }
  398. var previousFieldEditable = false;
  399. for (var j = 0, field; field = input.fieldRow[j]; j++) {
  400. if (j != 0) {
  401. input.fieldWidth += Blockly.BlockSvg.SEP_SPACE_X;
  402. }
  403. // Get the dimensions of the field.
  404. var fieldSize = field.getSize();
  405. field.renderWidth = fieldSize.width;
  406. field.renderSep = (previousFieldEditable && field.EDITABLE) ?
  407. Blockly.BlockSvg.SEP_SPACE_X : 0;
  408. input.fieldWidth += field.renderWidth + field.renderSep;
  409. row.height = Math.max(row.height, fieldSize.height);
  410. previousFieldEditable = field.EDITABLE;
  411. }
  412. if (row.type != Blockly.BlockSvg.INLINE) {
  413. if (row.type == Blockly.NEXT_STATEMENT) {
  414. hasStatement = true;
  415. fieldStatementWidth = Math.max(fieldStatementWidth, input.fieldWidth);
  416. } else {
  417. if (row.type == Blockly.INPUT_VALUE) {
  418. hasValue = true;
  419. } else if (row.type == Blockly.DUMMY_INPUT) {
  420. hasDummy = true;
  421. }
  422. fieldValueWidth = Math.max(fieldValueWidth, input.fieldWidth);
  423. }
  424. }
  425. }
  426. // Make inline rows a bit thicker in order to enclose the values.
  427. for (var y = 0, row; row = inputRows[y]; y++) {
  428. row.thicker = false;
  429. if (row.type == Blockly.BlockSvg.INLINE) {
  430. for (var z = 0, input; input = row[z]; z++) {
  431. if (input.type == Blockly.INPUT_VALUE) {
  432. row.height += 2 * Blockly.BlockSvg.INLINE_PADDING_Y;
  433. row.thicker = true;
  434. break;
  435. }
  436. }
  437. }
  438. }
  439. // Compute the statement edge.
  440. // This is the width of a block where statements are nested.
  441. inputRows.statementEdge = 2 * Blockly.BlockSvg.SEP_SPACE_X +
  442. fieldStatementWidth;
  443. // Compute the preferred right edge. Inline blocks may extend beyond.
  444. // This is the width of the block where external inputs connect.
  445. if (hasStatement) {
  446. inputRows.rightEdge = Math.max(inputRows.rightEdge,
  447. inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH);
  448. }
  449. if (hasValue) {
  450. inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth +
  451. Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.TAB_WIDTH);
  452. } else if (hasDummy) {
  453. inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth +
  454. Blockly.BlockSvg.SEP_SPACE_X * 2);
  455. }
  456. inputRows.hasValue = hasValue;
  457. inputRows.hasStatement = hasStatement;
  458. inputRows.hasDummy = hasDummy;
  459. return inputRows;
  460. };
  461. /**
  462. * Draw the path of the block.
  463. * Move the fields to the correct locations.
  464. * @param {number} iconWidth Offset of first row due to icons.
  465. * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each
  466. * containing position information.
  467. * @private
  468. */
  469. Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
  470. this.startHat_ = false;
  471. // Reset the height to zero and let the rendering process add in
  472. // portions of the block height as it goes. (e.g. hats, inputs, etc.)
  473. this.height = 0;
  474. // Should the top and bottom left corners be rounded or square?
  475. if (this.outputConnection) {
  476. this.squareTopLeftCorner_ = true;
  477. this.squareBottomLeftCorner_ = true;
  478. } else {
  479. this.squareTopLeftCorner_ = false;
  480. this.squareBottomLeftCorner_ = false;
  481. // If this block is in the middle of a stack, square the corners.
  482. if (this.previousConnection) {
  483. var prevBlock = this.previousConnection.targetBlock();
  484. if (prevBlock && prevBlock.getNextBlock() == this) {
  485. this.squareTopLeftCorner_ = true;
  486. }
  487. } else if (Blockly.BlockSvg.START_HAT) {
  488. // No output or previous connection.
  489. this.squareTopLeftCorner_ = true;
  490. this.startHat_ = true;
  491. this.height += Blockly.BlockSvg.START_HAT_HEIGHT;
  492. inputRows.rightEdge = Math.max(inputRows.rightEdge, 100);
  493. }
  494. var nextBlock = this.getNextBlock();
  495. if (nextBlock) {
  496. this.squareBottomLeftCorner_ = true;
  497. }
  498. }
  499. // Assemble the block's path.
  500. var steps = [];
  501. var inlineSteps = [];
  502. // The highlighting applies to edges facing the upper-left corner.
  503. // Since highlighting is a two-pixel wide border, it would normally overhang
  504. // the edge of the block by a pixel. So undersize all measurements by a pixel.
  505. var highlightSteps = [];
  506. var highlightInlineSteps = [];
  507. this.renderDrawTop_(steps, highlightSteps, inputRows.rightEdge);
  508. var cursorY = this.renderDrawRight_(steps, highlightSteps, inlineSteps,
  509. highlightInlineSteps, inputRows, iconWidth);
  510. this.renderDrawBottom_(steps, highlightSteps, cursorY);
  511. this.renderDrawLeft_(steps, highlightSteps);
  512. var pathString = steps.join(' ') + '\n' + inlineSteps.join(' ');
  513. this.svgPath_.setAttribute('d', pathString);
  514. this.svgPathDark_.setAttribute('d', pathString);
  515. pathString = highlightSteps.join(' ') + '\n' + highlightInlineSteps.join(' ');
  516. this.svgPathLight_.setAttribute('d', pathString);
  517. if (this.RTL) {
  518. // Mirror the block's path.
  519. this.svgPath_.setAttribute('transform', 'scale(-1 1)');
  520. this.svgPathLight_.setAttribute('transform', 'scale(-1 1)');
  521. this.svgPathDark_.setAttribute('transform', 'translate(1,1) scale(-1 1)');
  522. }
  523. };
  524. /**
  525. * Update all of the connections on this block with the new locations calculated
  526. * in renderCompute. Also move all of the connected blocks based on the new
  527. * connection locations.
  528. * @private
  529. */
  530. Blockly.BlockSvg.prototype.renderMoveConnections_ = function() {
  531. var blockTL = this.getRelativeToSurfaceXY();
  532. // Don't tighten previous or output connecitons because they are inferior
  533. // connections.
  534. if (this.previousConnection) {
  535. this.previousConnection.moveToOffset(blockTL);
  536. }
  537. if (this.outputConnection) {
  538. this.outputConnection.moveToOffset(blockTL);
  539. }
  540. for (var i = 0; i < this.inputList.length; i++) {
  541. var conn = this.inputList[i].connection;
  542. if (conn) {
  543. conn.moveToOffset(blockTL);
  544. if (conn.isConnected()) {
  545. conn.tighten_();
  546. }
  547. }
  548. }
  549. if (this.nextConnection) {
  550. this.nextConnection.moveToOffset(blockTL);
  551. if (this.nextConnection.isConnected()) {
  552. this.nextConnection.tighten_();
  553. }
  554. }
  555. };
  556. /**
  557. * Render the top edge of the block.
  558. * @param {!Array.<string>} steps Path of block outline.
  559. * @param {!Array.<string>} highlightSteps Path of block highlights.
  560. * @param {number} rightEdge Minimum width of block.
  561. * @private
  562. */
  563. Blockly.BlockSvg.prototype.renderDrawTop_ =
  564. function(steps, highlightSteps, rightEdge) {
  565. /* eslint-disable indent */
  566. // Position the cursor at the top-left starting point.
  567. if (this.squareTopLeftCorner_) {
  568. steps.push('m 0,0');
  569. highlightSteps.push('m 0.5,0.5');
  570. if (this.startHat_) {
  571. steps.push(Blockly.BlockSvg.START_HAT_PATH);
  572. highlightSteps.push(this.RTL ?
  573. Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL :
  574. Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR);
  575. }
  576. } else {
  577. steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
  578. highlightSteps.push(this.RTL ?
  579. Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL :
  580. Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR);
  581. // Top-left rounded corner.
  582. steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER);
  583. highlightSteps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT);
  584. }
  585. // Top edge.
  586. if (this.previousConnection) {
  587. steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15);
  588. highlightSteps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15);
  589. steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT);
  590. highlightSteps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT);
  591. var connectionX = (this.RTL ?
  592. -Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH);
  593. this.previousConnection.setOffsetInBlock(connectionX, 0);
  594. }
  595. steps.push('H', rightEdge);
  596. highlightSteps.push('H', rightEdge - 0.5);
  597. this.width = rightEdge;
  598. }; /* eslint-enable indent */
  599. /**
  600. * Render the right edge of the block.
  601. * @param {!Array.<string>} steps Path of block outline.
  602. * @param {!Array.<string>} highlightSteps Path of block highlights.
  603. * @param {!Array.<string>} inlineSteps Inline block outlines.
  604. * @param {!Array.<string>} highlightInlineSteps Inline block highlights.
  605. * @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each
  606. * containing position information.
  607. * @param {number} iconWidth Offset of first row due to icons.
  608. * @return {number} Height of block.
  609. * @private
  610. */
  611. Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, highlightSteps,
  612. inlineSteps, highlightInlineSteps, inputRows, iconWidth) {
  613. var cursorX;
  614. var cursorY = 0;
  615. var connectionX, connectionY;
  616. for (var y = 0, row; row = inputRows[y]; y++) {
  617. cursorX = Blockly.BlockSvg.SEP_SPACE_X;
  618. if (y == 0) {
  619. cursorX += this.RTL ? -iconWidth : iconWidth;
  620. }
  621. highlightSteps.push('M', (inputRows.rightEdge - 0.5) + ',' +
  622. (cursorY + 0.5));
  623. if (this.isCollapsed()) {
  624. // Jagged right edge.
  625. var input = row[0];
  626. var fieldX = cursorX;
  627. var fieldY = cursorY;
  628. this.renderFields_(input.fieldRow, fieldX, fieldY);
  629. steps.push(Blockly.BlockSvg.JAGGED_TEETH);
  630. highlightSteps.push('h 8');
  631. var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT;
  632. steps.push('v', remainder);
  633. if (this.RTL) {
  634. highlightSteps.push('v 3.9 l 7.2,3.4 m -14.5,8.9 l 7.3,3.5');
  635. highlightSteps.push('v', remainder - 0.7);
  636. }
  637. this.width += Blockly.BlockSvg.JAGGED_TEETH_WIDTH;
  638. } else if (row.type == Blockly.BlockSvg.INLINE) {
  639. // Inline inputs.
  640. for (var x = 0, input; input = row[x]; x++) {
  641. var fieldX = cursorX;
  642. var fieldY = cursorY;
  643. if (row.thicker) {
  644. // Lower the field slightly.
  645. fieldY += Blockly.BlockSvg.INLINE_PADDING_Y;
  646. }
  647. // TODO: Align inline field rows (left/right/centre).
  648. cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY);
  649. if (input.type != Blockly.DUMMY_INPUT) {
  650. cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X;
  651. }
  652. if (input.type == Blockly.INPUT_VALUE) {
  653. inlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X) +
  654. ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y));
  655. inlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 -
  656. input.renderWidth);
  657. inlineSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN);
  658. inlineSteps.push('v', input.renderHeight + 1 -
  659. Blockly.BlockSvg.TAB_HEIGHT);
  660. inlineSteps.push('h', input.renderWidth + 2 -
  661. Blockly.BlockSvg.TAB_WIDTH);
  662. inlineSteps.push('z');
  663. if (this.RTL) {
  664. // Highlight right edge, around back of tab, and bottom.
  665. highlightInlineSteps.push('M',
  666. (cursorX - Blockly.BlockSvg.SEP_SPACE_X - 2.5 +
  667. Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' +
  668. (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5));
  669. highlightInlineSteps.push(
  670. Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
  671. highlightInlineSteps.push('v',
  672. input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 2.5);
  673. highlightInlineSteps.push('h',
  674. input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 2);
  675. } else {
  676. // Highlight right edge, bottom.
  677. highlightInlineSteps.push('M',
  678. (cursorX - Blockly.BlockSvg.SEP_SPACE_X + 0.5) + ',' +
  679. (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5));
  680. highlightInlineSteps.push('v', input.renderHeight + 1);
  681. highlightInlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 -
  682. input.renderWidth);
  683. // Short highlight glint at bottom of tab.
  684. highlightInlineSteps.push('M',
  685. (cursorX - input.renderWidth - Blockly.BlockSvg.SEP_SPACE_X +
  686. 0.9) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y +
  687. Blockly.BlockSvg.TAB_HEIGHT - 0.7));
  688. highlightInlineSteps.push('l',
  689. (Blockly.BlockSvg.TAB_WIDTH * 0.46) + ',-2.1');
  690. }
  691. // Create inline input connection.
  692. if (this.RTL) {
  693. connectionX = -cursorX -
  694. Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X +
  695. input.renderWidth + 1;
  696. } else {
  697. connectionX = cursorX +
  698. Blockly.BlockSvg.TAB_WIDTH - Blockly.BlockSvg.SEP_SPACE_X -
  699. input.renderWidth - 1;
  700. }
  701. connectionY = cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1;
  702. input.connection.setOffsetInBlock(connectionX, connectionY);
  703. }
  704. }
  705. cursorX = Math.max(cursorX, inputRows.rightEdge);
  706. this.width = Math.max(this.width, cursorX);
  707. steps.push('H', cursorX);
  708. highlightSteps.push('H', cursorX - 0.5);
  709. steps.push('v', row.height);
  710. if (this.RTL) {
  711. highlightSteps.push('v', row.height - 1);
  712. }
  713. } else if (row.type == Blockly.INPUT_VALUE) {
  714. // External input.
  715. var input = row[0];
  716. var fieldX = cursorX;
  717. var fieldY = cursorY;
  718. if (input.align != Blockly.ALIGN_LEFT) {
  719. var fieldRightX = inputRows.rightEdge - input.fieldWidth -
  720. Blockly.BlockSvg.TAB_WIDTH - 2 * Blockly.BlockSvg.SEP_SPACE_X;
  721. if (input.align == Blockly.ALIGN_RIGHT) {
  722. fieldX += fieldRightX;
  723. } else if (input.align == Blockly.ALIGN_CENTRE) {
  724. fieldX += fieldRightX / 2;
  725. }
  726. }
  727. this.renderFields_(input.fieldRow, fieldX, fieldY);
  728. steps.push(Blockly.BlockSvg.TAB_PATH_DOWN);
  729. var v = row.height - Blockly.BlockSvg.TAB_HEIGHT;
  730. steps.push('v', v);
  731. if (this.RTL) {
  732. // Highlight around back of tab.
  733. highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
  734. highlightSteps.push('v', v + 0.5);
  735. } else {
  736. // Short highlight glint at bottom of tab.
  737. highlightSteps.push('M', (inputRows.rightEdge - 5) + ',' +
  738. (cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.7));
  739. highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.46) +
  740. ',-2.1');
  741. }
  742. // Create external input connection.
  743. connectionX = this.RTL ? -inputRows.rightEdge - 1 :
  744. inputRows.rightEdge + 1;
  745. input.connection.setOffsetInBlock(connectionX, cursorY);
  746. if (input.connection.isConnected()) {
  747. this.width = Math.max(this.width, inputRows.rightEdge +
  748. input.connection.targetBlock().getHeightWidth().width -
  749. Blockly.BlockSvg.TAB_WIDTH + 1);
  750. }
  751. } else if (row.type == Blockly.DUMMY_INPUT) {
  752. // External naked field.
  753. var input = row[0];
  754. var fieldX = cursorX;
  755. var fieldY = cursorY;
  756. if (input.align != Blockly.ALIGN_LEFT) {
  757. var fieldRightX = inputRows.rightEdge - input.fieldWidth -
  758. 2 * Blockly.BlockSvg.SEP_SPACE_X;
  759. if (inputRows.hasValue) {
  760. fieldRightX -= Blockly.BlockSvg.TAB_WIDTH;
  761. }
  762. if (input.align == Blockly.ALIGN_RIGHT) {
  763. fieldX += fieldRightX;
  764. } else if (input.align == Blockly.ALIGN_CENTRE) {
  765. fieldX += fieldRightX / 2;
  766. }
  767. }
  768. this.renderFields_(input.fieldRow, fieldX, fieldY);
  769. steps.push('v', row.height);
  770. if (this.RTL) {
  771. highlightSteps.push('v', row.height - 1);
  772. }
  773. } else if (row.type == Blockly.NEXT_STATEMENT) {
  774. // Nested statement.
  775. var input = row[0];
  776. if (y == 0) {
  777. // If the first input is a statement stack, add a small row on top.
  778. steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y);
  779. if (this.RTL) {
  780. highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1);
  781. }
  782. cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
  783. }
  784. var fieldX = cursorX;
  785. var fieldY = cursorY;
  786. if (input.align != Blockly.ALIGN_LEFT) {
  787. var fieldRightX = inputRows.statementEdge - input.fieldWidth -
  788. 2 * Blockly.BlockSvg.SEP_SPACE_X;
  789. if (input.align == Blockly.ALIGN_RIGHT) {
  790. fieldX += fieldRightX;
  791. } else if (input.align == Blockly.ALIGN_CENTRE) {
  792. fieldX += fieldRightX / 2;
  793. }
  794. }
  795. this.renderFields_(input.fieldRow, fieldX, fieldY);
  796. cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH;
  797. steps.push('H', cursorX);
  798. steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER);
  799. steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS);
  800. steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER);
  801. steps.push('H', inputRows.rightEdge);
  802. if (this.RTL) {
  803. highlightSteps.push('M',
  804. (cursorX - Blockly.BlockSvg.NOTCH_WIDTH +
  805. Blockly.BlockSvg.DISTANCE_45_OUTSIDE) +
  806. ',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE));
  807. highlightSteps.push(
  808. Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL);
  809. highlightSteps.push('v',
  810. row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS);
  811. highlightSteps.push(
  812. Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL);
  813. highlightSteps.push('H', inputRows.rightEdge - 0.5);
  814. } else {
  815. highlightSteps.push('M',
  816. (cursorX - Blockly.BlockSvg.NOTCH_WIDTH +
  817. Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' +
  818. (cursorY + row.height - Blockly.BlockSvg.DISTANCE_45_OUTSIDE));
  819. highlightSteps.push(
  820. Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR);
  821. highlightSteps.push('H', inputRows.rightEdge - 0.5);
  822. }
  823. // Create statement connection.
  824. connectionX = this.RTL ? -cursorX : cursorX + 1;
  825. input.connection.setOffsetInBlock(connectionX, cursorY + 1);
  826. if (input.connection.isConnected()) {
  827. this.width = Math.max(this.width, inputRows.statementEdge +
  828. input.connection.targetBlock().getHeightWidth().width);
  829. }
  830. if (y == inputRows.length - 1 ||
  831. inputRows[y + 1].type == Blockly.NEXT_STATEMENT) {
  832. // If the final input is a statement stack, add a small row underneath.
  833. // Consecutive statement stacks are also separated by a small divider.
  834. steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y);
  835. if (this.RTL) {
  836. highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1);
  837. }
  838. cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
  839. }
  840. }
  841. cursorY += row.height;
  842. }
  843. if (!inputRows.length) {
  844. cursorY = Blockly.BlockSvg.MIN_BLOCK_Y;
  845. steps.push('V', cursorY);
  846. if (this.RTL) {
  847. highlightSteps.push('V', cursorY - 1);
  848. }
  849. }
  850. return cursorY;
  851. };
  852. /**
  853. * Render the bottom edge of the block.
  854. * @param {!Array.<string>} steps Path of block outline.
  855. * @param {!Array.<string>} highlightSteps Path of block highlights.
  856. * @param {number} cursorY Height of block.
  857. * @private
  858. */
  859. Blockly.BlockSvg.prototype.renderDrawBottom_ =
  860. function(steps, highlightSteps, cursorY) {
  861. /* eslint-disable indent */
  862. this.height += cursorY + 1; // Add one for the shadow.
  863. if (this.nextConnection) {
  864. steps.push('H', (Blockly.BlockSvg.NOTCH_WIDTH + (this.RTL ? 0.5 : - 0.5)) +
  865. ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT);
  866. // Create next block connection.
  867. var connectionX;
  868. if (this.RTL) {
  869. connectionX = -Blockly.BlockSvg.NOTCH_WIDTH;
  870. } else {
  871. connectionX = Blockly.BlockSvg.NOTCH_WIDTH;
  872. }
  873. this.nextConnection.setOffsetInBlock(connectionX, cursorY + 1);
  874. this.height += 4; // Height of tab.
  875. }
  876. // Should the bottom-left corner be rounded or square?
  877. if (this.squareBottomLeftCorner_) {
  878. steps.push('H 0');
  879. if (!this.RTL) {
  880. highlightSteps.push('M', '0.5,' + (cursorY - 0.5));
  881. }
  882. } else {
  883. steps.push('H', Blockly.BlockSvg.CORNER_RADIUS);
  884. steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
  885. Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' +
  886. Blockly.BlockSvg.CORNER_RADIUS + ',-' +
  887. Blockly.BlockSvg.CORNER_RADIUS);
  888. if (!this.RTL) {
  889. highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' +
  890. (cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE));
  891. highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' +
  892. (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' +
  893. '0.5,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS));
  894. }
  895. }
  896. }; /* eslint-enable indent */
  897. /**
  898. * Render the left edge of the block.
  899. * @param {!Array.<string>} steps Path of block outline.
  900. * @param {!Array.<string>} highlightSteps Path of block highlights.
  901. * @private
  902. */
  903. Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, highlightSteps) {
  904. if (this.outputConnection) {
  905. // Create output connection.
  906. this.outputConnection.setOffsetInBlock(0, 0);
  907. steps.push('V', Blockly.BlockSvg.TAB_HEIGHT);
  908. steps.push('c 0,-10 -' + Blockly.BlockSvg.TAB_WIDTH + ',8 -' +
  909. Blockly.BlockSvg.TAB_WIDTH + ',-7.5 s ' + Blockly.BlockSvg.TAB_WIDTH +
  910. ',2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5');
  911. if (this.RTL) {
  912. highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.25) + ',8.4');
  913. highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1');
  914. } else {
  915. highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1.5);
  916. highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) +
  917. ',-0.5 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) +
  918. ',-5.5 0,-11');
  919. highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) +
  920. ',1 V 0.5 H 1');
  921. }
  922. this.width += Blockly.BlockSvg.TAB_WIDTH;
  923. } else if (!this.RTL) {
  924. if (this.squareTopLeftCorner_) {
  925. // Statement block in a stack.
  926. highlightSteps.push('V', 0.5);
  927. } else {
  928. highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
  929. }
  930. }
  931. steps.push('z');
  932. };