Connection shapes

There are several ways you can customize the ways connections look, each with increasing difficulty. All of them require creating a custom renderer.

Basic dimensions

connections with different dimensions

You can customize connections by changing their width or height, while maintaining the same basic shape. To do this, you need to create a custom constant provider component, and override some constants.

Different renderers define and use different constants, so check out the reference documentation for your super class:

For the base renderer, you can override NOTCH_WIDTH and NOTCH_HEIGHT for next and previous connections, and TAB_WIDTH and TAB_HEIGHT for input and output connections.

class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
  constructor() {
    super();
    this.NOTCH_WIDTH = 20;
    this.NOTCH_HEIGHT = 10;
    this.TAB_HEIGHT = 8;
  }
}

Basic shapes

connections with different shapes

You can customize connections by overriding their basic shape. Basic shapes have a height, a width, and two paths.

Each path draws the same shape, but from opposite ends!

a notch drawn from both directions

This is necessary because as the drawer draws the outline of the block, it draws each kind of connection in both directions. For example, previous connections are drawn from left to right, but next connections are drawn from right to left. So you need to provide paths for both of those cases.

the direction a block gets drawn in

You can override the makeNotch method for next and previous connections, and the makePuzzleTab method for input and output connections.

class CustomConstantProvider extends Blockly.blockRendering.ConstantProvider {
  makePuzzleTab() {
    const width = this.TAB_WIDTH;
    const height = this.TAB_HEIGHT;
    return {
      type: this.SHAPES.PUZZLE,
      width,
      height,
      pathUp: Blockly.utils.svgPaths.line([
          Blockly.utils.svgPaths.point(-width, -height / 2),
          Blockly.utils.svgPaths.point(width, -height / 2)]),
      pathDown: Blockly.utils.svgPaths.line([
          Blockly.utils.svgPaths.point(-width, height / 2),
          Blockly.utils.svgPaths.point(width, height / 2)]),
    };
  }
}

Check out the MDN SVG path documentation for information about how to define path strings. The Blockly.utils.svgPaths namespace is provided as a thin wrapper around these strings to make them more readable.

Shapes for connection checks

different connections with different shapes

You can customize connections by changing the shape based on the connection's connection check.

This lets you create different shapes to represent different data types. For example, strings could be represented by triangular connections, while booleans are represented by round connections.

To supply different shapes for different connection checks, you need to override the shapeFor method. The shapes returned should be initialized in init.

See basic shapes for information about what kinds of shapes are supported.

export class ConstantProvider extends Blockly.blockRendering.BaseConstantProvider {
  shapeFor(connection) {
    let check = connection.getCheck();
    // For connections with no check, match any child block.
    if (!check && connection.targetConnection) {
      check = connection.targetConnection.getCheck();
    }

    if (check && check.includes('String')) return this.TRIANGULAR_TAB;
    if (check && check.includes('Boolean')) return this.ROUND_TAB;

    return super.shapeFor(connection);
  }
}

Custom inputs

You can customize connection shapes by creating an entirely custom input. This is only done if you want some connections to look different from others, but you don't want it to be based on the connection check.

For example, if you want some value inputs to be indented like statement inputs, you can create a custom input to support this.

Create a custom input class

Follow the steps for creating a custom input.

Create a measurable

You need to create a measurable to represent your custom input.

Your custom input measurable should inherit from Blockly.blockRendering.InputConnection. It can also include whatever extra measurement data you need to draw the input's shape.

export class CustomInputMeasurable extends Blockly.blockRendering.InputConnection {
  constructor(constants, input) {
    super(constants, input);

    // Any extra measurement data...
  }
}

Instantiate your measurable

Your render info needs to instantiate your custom measurable. To do this, you need to override the addInput_ method.

export class RenderInfo extends Blockly.blockRendering.RenderInfo {
  addInput_(input, activeRow) {
    if (input instanceof CustomInput) {
      activeRow.elements.push(new CustomInputMeasurable(this.constants_, input));
    }
    super.addInput_(input, activeRow);
  }
}

Optionally create a row

By default, inputs don't create new rows. If you want your input to trigger the end of a row, you need to override the shouldStartNewRow_ method of your render info.

export class RenderInfo extends Blockly.blockRendering.RenderInfo {
  shouldStartNewRow_(currInput, prevInput) {
    if (prevInput instanceof CustomInput) return true;
    return super.shouldStartNewRow_(currInput, prevInput);
  }
}

Optionally create a shape for your input

It is a good idea to store the shape of your input in a constant, just like we do for notches and puzzle tabs. This keeps your code organized, and makes it easier to modify later.

Draw the input

Lastly, you need to modify your drawer to draw the shape.

Custom inputs can either:

  • Affect the outline of your block, like statement inputs

    image of outline inputs

  • Or affect the internals of your block, like inline value inputs

    image of internal inputs

If the input affects the outline of your block, override drawOutline_, otherwise, override drawInternals_.

export class Drawer extends Blockly.blockRendering.Drawer {
  drawOutline_() {
    this.drawTop_();
    for (let r = 1; r < this.info_.rows.length - 1; r++) {
      const row = this.info_.rows[r];

      // Insert checks for your input here!
      if (row.getLastInput() instanceof CustomInputMeasurable) {
        this.drawCustomInput(row);
      } else if (row.hasJaggedEdge) {
        this.drawJaggedEdge_(row);
      } else if (row.hasStatement) {
        this.drawStatementInput_(row);
      } else if (row.hasExternalInput) {
        this.drawValueInput_(row);
      } else {
        this.drawRightSideRow_(row);
      }
    }
    this.drawBottom_();
    this.drawLeft_();
  }

  protected drawInternals_() {
    for (const row of rows) {
      for (const elem of row) {

        // Insert checks for your input here!
        if (elem instanceof CustomInputMeasurable) {
          this.drawCustomInput(elem);
        }

        if (Types.isInlineInput(elem)) {
          this.drawInlineInput_(elem as InlineInput);
        } else if (Types.isIcon(elem) || Types.isField(elem)) {
          this.layoutField_(elem as Field | Icon);
        }
      }
    }
  }
}