From cef4d1592f34e0ca0f3caab7c761c98d21794585 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 15 Jun 2026 06:23:33 +0000 Subject: [PATCH] fix(scratch-vm): render extension HAT/EVENT blocks as hats in modern Blockly Extension hat blocks (koshien connect_game, micro:bit / face-sensing / boost / ev3 "when ..." blocks, etc.) rendered with a flat top instead of the cap-hat shape. In modern Blockly (scratch-blocks v2) the cap hat is only drawn when a block carries the `shape_hat` extension (which sets block.hat = 'cap'); a missing previousConnection alone is no longer enough because ADD_START_HATS defaults to false. The built-in event blocks declare extensions: ['colours_event', 'shape_hat'], so _convertBlockForScratchBlocks now adds 'shape_hat' to extension HAT/EVENT blocks the same way. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/rules/scratch-vm/development.md | 1 + packages/scratch-vm/src/engine/runtime.js | 10 ++++ .../test/unit/runtime_extension_hat_shape.js | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 packages/scratch-vm/test/unit/runtime_extension_hat_shape.js diff --git a/.claude/rules/scratch-vm/development.md b/.claude/rules/scratch-vm/development.md index a2799ad5ff2..075d675543f 100644 --- a/.claude/rules/scratch-vm/development.md +++ b/.claude/rules/scratch-vm/development.md @@ -216,6 +216,7 @@ The mesh v2 extension uses AWS AppSync for real-time collaboration: | `src/blocks/scratch3_operators.js` | regex support | operator_contains で正規表現マッチングをサポート | | `src/engine/comment.js` | toXML modernization | Blockly v12 対応: `pinned="${!minimized}"` (cherry-pick from upstream spork@29bdbd1fe) + (0,0) 時の x/y 属性省略 (Smalruby 独自) | | `src/engine/runtime.js` | toolboxitemid for extension categories | Blockly v12 対応: 拡張機能のカテゴリ XML に `toolboxitemid` 属性を追加。Blockly v12 の ContinuousToolbox は `toolboxitemid` から id を読むため、未指定だと `blockly-XXX` の auto-id が StatusIndicatorLabel.extensionId に伝搬し、`!` 接続モーダルが拡張機能を見つけられず scanning で固まる | +| `src/engine/runtime.js` | extension hat shape (modern Blockly) | Blockly v12 対応: 拡張機能の HAT/EVENT ブロックの blockJSON に `shape_hat` extension を付与。modern Blockly は `block.hat === 'cap'`(= `shape_hat` extension)でのみ帽子型を描画し、`ADD_START_HATS` は既定 false。未付与だと甲子園 `connect_game` や micro:bit/顔認識の `when ...` ブロックが帽子型にならず平らな上端で描画される | ### 関連ファイル diff --git a/packages/scratch-vm/src/engine/runtime.js b/packages/scratch-vm/src/engine/runtime.js index 179df296ba9..2b619ab54d1 100644 --- a/packages/scratch-vm/src/engine/runtime.js +++ b/packages/scratch-vm/src/engine/runtime.js @@ -1156,6 +1156,16 @@ class Runtime extends EventEmitter { } blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.nextStatement = null; // null = available connection; undefined = terminal + // === Smalruby: Start of extension hat shape (modern Blockly) === + // In modern Blockly (scratch-blocks v2) the cap-hat shape is only + // drawn when the block carries the `shape_hat` extension (which sets + // block.hat = 'cap'); a missing previousConnection alone is no longer + // enough (ADD_START_HATS defaults to false). The built-in event blocks + // declare extensions: ['colours_event', 'shape_hat'], so mirror that + // here for extension HAT/EVENT blocks (e.g. koshien connect_game, + // micro:bit/face-sensing "when ..." blocks) so they render as hats. + blockJSON.extensions = (blockJSON.extensions || []).concat('shape_hat'); + // === Smalruby: End of extension hat shape (modern Blockly) === break; case BlockType.CONDITIONAL: case BlockType.LOOP: diff --git a/packages/scratch-vm/test/unit/runtime_extension_hat_shape.js b/packages/scratch-vm/test/unit/runtime_extension_hat_shape.js new file mode 100644 index 00000000000..54a0efff865 --- /dev/null +++ b/packages/scratch-vm/test/unit/runtime_extension_hat_shape.js @@ -0,0 +1,54 @@ +const test = require('tap').test; +const Runtime = require('../../src/engine/runtime'); +const BlockType = require('../../src/extension-support/block-type'); + +const categoryInfo = { + id: 'testcat', + name: 'Test', + color1: '#111111', + color2: '#222222', + color3: '#333333', + blocks: [], + customFieldTypes: {}, + menus: [], + menuInfo: {} +}; + +const convert = (rt, blockInfo) => rt._convertForScratchBlocks(blockInfo, categoryInfo); + +// Modern Blockly (scratch-blocks v2) only draws the cap-hat shape for blocks +// carrying the `shape_hat` extension. Verify the extension-block conversion adds +// it for HAT/EVENT blocks (so e.g. koshien connect_game renders as a hat) and +// does not turn COMMAND blocks into hats. +test('extension HAT/EVENT blocks get the shape_hat extension', t => { + const rt = new Runtime(); + + const hat = convert(rt, {opcode: 'whenX', blockType: BlockType.HAT, text: 'when X', arguments: {}}); + t.ok(hat.json.extensions, 'HAT has an extensions array'); + t.ok(hat.json.extensions.includes('shape_hat'), 'HAT has shape_hat'); + t.equal(hat.json.previousStatement, undefined, 'HAT has no previous connection'); + t.equal(hat.json.nextStatement, null, 'HAT has a next connection'); + + const event = convert(rt, {opcode: 'onX', blockType: BlockType.EVENT, text: 'on X', arguments: {}}); + t.ok(event.json.extensions.includes('shape_hat'), 'EVENT has shape_hat'); + + const cmd = convert(rt, {opcode: 'doX', blockType: BlockType.COMMAND, text: 'do X', arguments: {}}); + t.notOk( + cmd.json.extensions && cmd.json.extensions.includes('shape_hat'), + 'COMMAND does not get shape_hat' + ); + t.equal(cmd.json.previousStatement, null, 'COMMAND keeps its previous connection'); + + // a HAT with an icon keeps the icon extension AND gains shape_hat + const hatWithIcon = convert(rt, { + opcode: 'whenIcon', + blockType: BlockType.HAT, + text: 'when icon', + arguments: {}, + blockIconURI: 'data:image/png;base64,AAAA' + }); + t.ok(hatWithIcon.json.extensions.includes('shape_hat'), 'icon HAT has shape_hat'); + t.ok(hatWithIcon.json.extensions.includes('scratch_extension'), 'icon HAT keeps scratch_extension'); + + t.end(); +});