diff --git a/packages/blockly/core/renderers/zelos/info.ts b/packages/blockly/core/renderers/zelos/info.ts index 97dd7020ab7..3e28032ee4b 100644 --- a/packages/blockly/core/renderers/zelos/info.ts +++ b/packages/blockly/core/renderers/zelos/info.ts @@ -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'; @@ -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. * @@ -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: @@ -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; } diff --git a/packages/blockly/tests/mocha/zelos_info_test.js b/packages/blockly/tests/mocha/zelos_info_test.js new file mode 100644 index 00000000000..222866b6f29 --- /dev/null +++ b/packages/blockly/tests/mocha/zelos_info_test.js @@ -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); + }); +});