Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions packages/blockly/core/renderers/zelos/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import type {BlockSvg} from '../../block_svg.js';
import {FieldImage} from '../../field_image.js';
import {FieldInput} from '../../field_input.js';
import {FieldLabel} from '../../field_label.js';
import {FieldTextInput} from '../../field_textinput.js';
import {Align} from '../../inputs/align.js';
import {DummyInput} from '../../inputs/dummy_input.js';
import {EndRowInput} from '../../inputs/end_row_input.js';
Expand All @@ -31,6 +31,25 @@ import {TopRow} from './measurables/top_row.js';
import type {PathObject} from './path_object.js';
import type {Renderer} from './renderer.js';

/**
* Horizontal inset from a round output-cap tip needed so content at the given
* vertical distance from the centerline stays inside the circle.
*
* @param radius Round cap radius (dynamic connection width).
* @param distanceFromCenter Vertical distance from the cap centerline.
* @returns Clearance from the tip to the content edge.
*/
export function roundCapClearance(
radius: number,
distanceFromCenter: number,
): number {
if (radius <= 0) {
return 0;
}
const y = Math.min(Math.abs(distanceFromCenter), radius);
return radius - Math.sqrt(radius * radius - y * y);
}

/**
* An object containing all sizing information needed to draw this block.
*
Expand Down Expand Up @@ -518,8 +537,7 @@ export class RenderInfo extends BaseRenderInfo {
const maxWidth = this.constants_.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH;
const width = this.height / 2 > maxWidth ? maxWidth : this.height / 2;
const topPadding = this.constants_.SMALL_PADDING;
const roundPadding =
width * (1 - Math.sin(Math.acos((width - topPadding) / width)));
const roundPadding = roundCapClearance(width, width - topPadding);
return connectionWidth - roundPadding;
}
default:
Expand Down Expand Up @@ -554,16 +572,30 @@ export class RenderInfo extends BaseRenderInfo {
this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][innerShape]
);
} else if (Types.isField(elem)) {
// Special case for text inputs.
// Special case for text and number inputs.
if (
outerShape === constants.SHAPES.ROUND &&
elem.field instanceof FieldTextInput
elem.field instanceof FieldInput
) {
return connectionWidth - 2.75 * constants.GRID_UNIT;
}
return (
connectionWidth - this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][0]
);
const minClearance =
this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][0];
let clearance = minClearance;
if (outerShape === constants.SHAPES.ROUND) {
// Tall rectangular fields like bitmaps and images need more than the
// centerline minimum so their corners stay inside dynamic round caps.
// Pure geometry would put corners directly on the path so add a bit
// of padding so they don't visually touch.
const calculatedClearance =
roundCapClearance(connectionWidth, elem.height / 2) +
constants.SMALL_PADDING;
clearance = Math.min(
connectionWidth,
Math.max(minClearance, calculatedClearance),
);
}
return connectionWidth - clearance;
} else if (Types.isIcon(elem)) {
return this.constants_.SMALL_PADDING;
}
Expand Down
56 changes: 56 additions & 0 deletions packages/blockly/tests/mocha/zelos_info_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {assert} from 'chai';
import {
sharedTestSetup,
sharedTestTeardown,
} from './test_helpers/setup_teardown.js';

suite('Zelos RenderInfo', function () {
setup(function () {
sharedTestSetup.call(this);
this.workspace = Blockly.inject('blocklyDiv', {renderer: 'zelos'});
Blockly.defineBlocksWithJsonArray([
{
'type': 'tall_round_reporter',
'message0': '%1',
'args0': [
{
'type': 'field_image',
'name': 'IMG',
// Layout uses width/height only; src need not resolve.
'src': 'about:blank',
'width': 75,
'height': 75,
'alt': 'A',
},
],
'output': null,
},
]);
});

teardown(function () {
sharedTestTeardown.call(this);
});

test('tall image on round reporter keeps corners inside the caps', function () {
const block = this.workspace.newBlock('tall_round_reporter');
block.initSvg();
block.render();

const size = block.getHeightWidth();
const fieldSize = block.getField('IMG').getSize();
const horizontalPad = size.width - fieldSize.width;

// Height-aware round-cap clearance for a 75px field (radius 42) needs
// more than the centerline-only pad (24px) but less than keeping the
// full caps (84px), so the rectangle corners stay inside the curve.
assert.isAbove(horizontalPad, 24);
assert.isBelow(horizontalPad, 84);
});
});