Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
6c5dfa008This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* Stack-chan Blockly blocks and JavaScript code generation.
*
* The generated program is a Stack-chan MOD module: the workspace code becomes
* the body of `export async function onContextCreated(robot)`. Event blocks
* (start / button / interval) register handlers on the robot context.
*/
const HELPER_HEX_TO_RGB = `function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16)
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
}`
// Event dispatch helpers. A driver exposes a single \`onEvent\`, so multiple
// blocks for the same driver (e.g. button press + release, or several IMU
// motions) must share one handler. Each helper stores per-key handlers on the
// driver object and installs one dispatching \`onEvent\` the first time.
const HELPER_ON_BUTTON = `function onButton(robot, name, edge, handler) {
const button = robot.input.button?.[name]
if (!button) return
;(button.__handlers ??= {})[edge] = handler
if (button.__wired) return
button.__wired = true
button.onEvent = (event) => button.__handlers[event.pressed ? 'press' : 'release']?.(event)
}`
const HELPER_ON_IMU = `function onImu(robot, motion, handler) {
const imu = robot.input.imu
if (!imu) return
;(imu.__handlers ??= {})[motion] = handler
if (imu.__wired) return
imu.__wired = true
imu.onEvent = (event) => imu.__handlers[event.motion]?.(event)
imu.start?.()
}`
const HELPER_ON_TOUCH_PANEL = `function onTouchPanel(robot, gesture, handler) {
const panel = robot.input.touchPanel
if (!panel) return
;(panel.__handlers ??= {})[gesture] = handler
if (panel.__wired) return
panel.__wired = true
panel.onEvent = (event) => panel.__handlers[event.gesture]?.(event)
}`
/**
* Wrap generated workspace code into a complete mod.js source.
* Imports and helpers are included only when the body references them.
*/
export function assembleModSource(body) {
const imports = []
if (/\bTimer\s*\./.test(body)) imports.push("import Timer from 'timer'")
const utilNames = ['randomBetween', 'wait'].filter((name) => new RegExp(`\\b${name}\\s*\\(`).test(body))
if (utilNames.length) imports.push(`import { ${utilNames.join(', ')} } from 'stackchan-util'`)
if (/\bEmotion\s*\./.test(body)) imports.push("import { Emotion } from 'face-state'")
const helpers = []
if (/\bhexToRgb\s*\(/.test(body)) helpers.push(HELPER_HEX_TO_RGB)
if (/\bonButton\s*\(/.test(body)) helpers.push(HELPER_ON_BUTTON)
if (/\bonImu\s*\(/.test(body)) helpers.push(HELPER_ON_IMU)
if (/\bonTouchPanel\s*\(/.test(body)) helpers.push(HELPER_ON_TOUCH_PANEL)
const indentedBody = body
.split('\n')
.map((line) => (line.length ? ` ${line}` : line))
.join('\n')
.replace(/\s+$/, '')
const sections = []
if (imports.length) sections.push(imports.join('\n'))
if (helpers.length) sections.push(helpers.join('\n\n'))
sections.push(`export async function onContextCreated(robot) {\n${indentedBody}\n}`)
return `${sections.join('\n\n')}\n`
}
export const EMOTION_OPTIONS = [
['ふつう', 'NEUTRAL'],
['うれしい', 'HAPPY'],
['おこった', 'ANGRY'],
['かなしい', 'SAD'],
['ねむい', 'SLEEPY'],
['こまった', 'DOUBTFUL'],
['さむい', 'COLD'],
['あつい', 'HOT'],
]
export const COLOR_OPTIONS = [
['白', '#ffffff'],
['黒', '#202020'],
['赤', '#ff4040'],
['オレンジ', '#ff9900'],
['黄', '#ffe040'],
['緑', '#40c040'],
['水色', '#30e0ff'],
['青', '#3060ff'],
['紫', '#9040ff'],
['ピンク', '#ff70d8'],
]
// Musical notes (C major, 2 octaves + top C) as dropdown label -> frequency Hz.
// robot.audio.tone() takes Hz, so the value is the frequency directly.
export const NOTE_OPTIONS = [
['ド4', '262'],
['レ4', '294'],
['ミ4', '330'],
['ファ4', '349'],
['ソ4', '392'],
['ラ4', '440'],
['シ4', '494'],
['ド5', '523'],
['レ5', '587'],
['ミ5', '659'],
['ファ5', '698'],
['ソ5', '784'],
['ラ5', '880'],
['シ5', '988'],
['ド6', '1047'],
]
const BLOCK_STYLE = {
event: 290,
face: 20,
speech: 160,
motion: 230,
light: 60,
ui: 200,
util: 330,
}
const BLOCK_DEFINITIONS = [
{
type: 'stackchan_on_start',
message0: 'スタートしたとき %1 %2',
args0: [{ type: 'input_dummy' }, { type: 'input_statement', name: 'DO' }],
colour: BLOCK_STYLE.event,
tooltip: 'MODが起動したときに実行します',
},
{
type: 'stackchan_on_button',
message0: 'ボタン %1 が %2 とき %3 %4',
args0: [
{
type: 'field_dropdown',
name: 'BUTTON',
options: [
['A', 'a'],
['B', 'b'],
['C', 'c'],
],
},
{
type: 'field_dropdown',
name: 'EDGE',
options: [
['押された', 'press'],
['離された', 'release'],
],
},
{ type: 'input_dummy' },
{ type: 'input_statement', name: 'DO' },
],
colour: BLOCK_STYLE.event,
tooltip: '本体のボタンが押された/離されたときに実行します',
},
{
type: 'stackchan_on_imu',
message0: '本体が %1 とき %2 %3',
args0: [
{
type: 'field_dropdown',
name: 'MOTION',
options: [
['ゆさぶられた', 'shake'],
['前に倒れた', 'fallenForward'],
['後ろに倒れた', 'fallenBackward'],
['左に倒れた', 'fallenLeft'],
['右に倒れた', 'fallenRight'],
['さかさまになった', 'upsideDown'],
],
},
{ type: 'input_dummy' },
{ type: 'input_statement', name: 'DO' },
],
colour: BLOCK_STYLE.event,
tooltip: '本体を動かしたとき(加速度センサー)に実行します',
},
{
type: 'stackchan_on_touch',
message0: '画面が %1 されたとき %2 %3',
args0: [
{
type: 'field_dropdown',
name: 'GESTURE',
options: [
['タッチ', 'press'],
['はなす', 'release'],
['右にスワイプ', 'forwardSwipe'],
['左にスワイプ', 'backwardSwipe'],
],
},
{ type: 'input_dummy' },
{ type: 'input_statement', name: 'DO' },
],
colour: BLOCK_STYLE.event,
tooltip: '画面(タッチパネル)を操作したときに実行します',
},
{
type: 'stackchan_on_drawer_button',
message0: 'ドロワーに %1 ボタンをつくって 押されたら %2 %3',
args0: [
{ type: 'field_input', name: 'LABEL', text: 'ボタン' },
{ type: 'input_dummy' },
{ type: 'input_statement', name: 'DO' },
],
colour: BLOCK_STYLE.event,
tooltip: '画面のドロワー(引き出しメニュー)にボタンを追加し、押されたときに実行します',
},
{
type: 'stackchan_every',
message0: '%1 秒ごとに %2 %3',
args0: [
{ type: 'field_number', name: 'SECONDS', value: 5, min: 0.1, precision: 0.1 },
{ type: 'input_dummy' },
{ type: 'input_statement', name: 'DO' },
],
colour: BLOCK_STYLE.event,
tooltip: '一定時間ごとにくり返し実行します',
},
{
type: 'stackchan_set_emotion',
message0: '表情を %1 にする',
args0: [{ type: 'field_dropdown', name: 'EMOTION', options: EMOTION_OPTIONS }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.face,
tooltip: 'スタックチャンの表情を変えます',
},
{
type: 'stackchan_set_color',
message0: '顔の %1 の色を %2 にする',
args0: [
{
type: 'field_dropdown',
name: 'KEY',
options: [
['線', 'primary'],
['背景', 'secondary'],
],
},
{ type: 'field_dropdown', name: 'COLOR', options: COLOR_OPTIONS },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.face,
tooltip: '顔の色を変えます',
},
{
type: 'stackchan_set_mouth',
message0: '口を %1 ひらく (0〜1)',
args0: [{ type: 'field_number', name: 'VALUE', value: 0.5, min: 0, max: 1, precision: 0.1 }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.face,
tooltip: '口のひらき具合を変えます',
},
{
type: 'stackchan_say',
message0: '%1 としゃべる',
args0: [{ type: 'input_value', name: 'TEXT' }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.speech,
tooltip: '音声合成でしゃべります(しゃべり終わるまで待ちます)',
},
{
type: 'stackchan_show_balloon',
message0: 'ふきだしで %1 を表示',
args0: [{ type: 'input_value', name: 'TEXT' }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.speech,
tooltip: 'ふきだしにテキストを表示します',
},
{
type: 'stackchan_hide_balloon',
message0: 'ふきだしを消す',
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.speech,
tooltip: 'ふきだしを消します',
},
{
type: 'stackchan_tone',
message0: '音 %1 を %2 ミリ秒ならす',
args0: [
{ type: 'field_dropdown', name: 'NOTE', options: NOTE_OPTIONS },
{ type: 'field_number', name: 'DURATION', value: 300, min: 1 },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.speech,
tooltip: 'ドレミの音をならします',
},
{
type: 'stackchan_look_at',
message0: '前 %1 左 %2 上 %3 の方を見る',
args0: [
{ type: 'field_number', name: 'X', value: 1 },
{ type: 'field_number', name: 'Y', value: 0 },
{ type: 'field_number', name: 'Z', value: 0 },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.motion,
tooltip: '指定した方向(メートル)に顔と視線を向けます',
},
{
type: 'stackchan_look_away',
message0: 'よそ見をやめる',
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.motion,
tooltip: '視線追従をやめて正面にもどります',
},
{
type: 'stackchan_set_torque',
message0: 'サーボの力を %1',
args0: [
{
type: 'field_dropdown',
name: 'TORQUE',
options: [
['入れる', 'true'],
['抜く', 'false'],
],
},
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.motion,
tooltip: 'サーボモーターのトルクを切り替えます',
},
{
type: 'stackchan_set_pose',
message0: '頭を 上下 %1 度 左右 %2 度 に向ける %3 秒かけて',
args0: [
{ type: 'field_number', name: 'PITCH', value: 0, min: -60, max: 60 },
{ type: 'field_number', name: 'YAW', value: 0, min: -60, max: 60 },
{ type: 'field_number', name: 'TIME', value: 0.5, min: 0, precision: 0.1 },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.motion,
tooltip: '頭の向きを指定した角度に動かします(動き終わるまで待ちます)',
},
{
type: 'stackchan_light_on',
message0: 'LED %1 を %2 で点ける',
args0: [
{ type: 'field_input', name: 'NAME', text: 'a' },
{ type: 'field_dropdown', name: 'COLOR', options: COLOR_OPTIONS },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.light,
tooltip: 'LEDを点灯します(LEDがある機種のみ)',
},
{
type: 'stackchan_light_off',
message0: 'LED %1 を消す',
args0: [{ type: 'field_input', name: 'NAME', text: 'a' }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.light,
tooltip: 'LEDを消灯します',
},
{
type: 'stackchan_light_rainbow',
message0: 'LED %1 を虹色に光らせる',
args0: [{ type: 'field_input', name: 'NAME', text: 'a' }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.light,
tooltip: 'LEDをレインボー点灯します',
},
{
type: 'stackchan_light_blink',
message0: 'LED %1 を %2 で %3 ミリ秒ごとに点滅',
args0: [
{ type: 'field_input', name: 'NAME', text: 'a' },
{ type: 'field_dropdown', name: 'COLOR', options: COLOR_OPTIONS },
{ type: 'field_number', name: 'INTERVAL', value: 250, min: 1 },
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.light,
tooltip: 'LEDを点滅させます(LEDがある機種のみ)',
},
{
type: 'stackchan_drawer_control',
message0: 'ドロワーを %1',
args0: [
{
type: 'field_dropdown',
name: 'ACTION',
options: [
['開く', 'openDrawer'],
['閉じる', 'closeDrawer'],
['切り替える', 'toggleDrawer'],
],
},
],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.ui,
tooltip: '画面のドロワー(引き出しメニュー)を開閉します',
},
{
type: 'stackchan_show_face',
message0: 'かおにもどす',
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.ui,
tooltip: 'メイン画面をかおの表示にもどします',
},
{
type: 'stackchan_wait',
message0: '%1 ミリ秒まつ',
args0: [{ type: 'field_number', name: 'DURATION', value: 1000, min: 0 }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.util,
tooltip: '指定した時間だけ待ちます',
},
{
type: 'stackchan_trace',
message0: 'ログに %1 を出す',
args0: [{ type: 'input_value', name: 'TEXT' }],
previousStatement: null,
nextStatement: null,
colour: BLOCK_STYLE.util,
tooltip: 'デバッグログに出力します',
},
{
type: 'stackchan_random',
message0: '%1 から %2 のランダムな数',
args0: [
{ type: 'field_number', name: 'MIN', value: 0 },
{ type: 'field_number', name: 'MAX', value: 1 },
],
output: 'Number',
colour: BLOCK_STYLE.util,
tooltip: '範囲内のランダムな数を返します',
},
]
// Escape a value for embedding inside a single-quoted JS string literal. Used
// for `field_input` values (e.g. the LED NAME) so a name containing ' or \
// cannot break the generated source.
export function escapeSingleQuoted(value) {
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")
}
function asyncHandlerBody(generator, block) {
const body = generator.statementToCode(block, 'DO')
return body.replace(/\s+$/, '')
}
// Build an event-handler arrow function that runs a statement body inside a
// fire-and-forget async IIFE (so `await` works and errors are traced).
// `param` is the callback parameter name ('event' for input events, '' for the
// drawer button which takes none).
function eventHandler(body, errorTag, param = 'event') {
const indented = body.replace(/^/gm, ' ')
return (
`(${param}) => {\n` +
` void (async () => {\n${indented}\n` +
` })().catch((error) => trace('${errorTag} handler failed: ' + error + '\\n'))\n` +
`}`
)
}
/**
* Register Stack-chan blocks and their JavaScript generators.
* `Blockly` is the UMD global; `generator` is javascript.javascriptGenerator.
*/
export function registerStackchanBlocks(Blockly, generator, Order) {
Blockly.defineBlocksWithJsonArray(BLOCK_DEFINITIONS)
const forBlock = generator.forBlock ?? generator
forBlock['stackchan_on_start'] = (block, gen) => {
const body = asyncHandlerBody(gen, block)
return `;(async () => {\n${body}\n})().catch((error) => trace('start handler failed: ' + error + '\\n'))\n`
}
forBlock['stackchan_on_button'] = (block, gen) => {
const button = block.getFieldValue('BUTTON')
const edge = block.getFieldValue('EDGE')
const body = asyncHandlerBody(gen, block)
return `onButton(robot, '${button}', '${edge}', ${eventHandler(body, `button ${button}`)})\n`
}
forBlock['stackchan_on_imu'] = (block, gen) => {
const motion = block.getFieldValue('MOTION')
const body = asyncHandlerBody(gen, block)
return `onImu(robot, '${motion}', ${eventHandler(body, 'imu')})\n`
}
forBlock['stackchan_on_touch'] = (block, gen) => {
const gesture = block.getFieldValue('GESTURE')
const body = asyncHandlerBody(gen, block)
return `onTouchPanel(robot, '${gesture}', ${eventHandler(body, 'touch')})\n`
}
forBlock['stackchan_on_drawer_button'] = (block, gen) => {
const label = escapeSingleQuoted(block.getFieldValue('LABEL'))
const key = escapeSingleQuoted(block.id)
const body = asyncHandlerBody(gen, block)
return `robot.ui.drawer?.addDrawerButton({ key: '${key}', label: '${label}', callback: ${eventHandler(body, 'drawer', '')} })\n`
}
forBlock['stackchan_every'] = (block, gen) => {
const seconds = Number(block.getFieldValue('SECONDS'))
const interval = Math.max(1, Math.round(seconds * 1000))
const body = asyncHandlerBody(gen, block)
return (
`Timer.repeat(() => {\n` +
` void (async () => {\n${body.replace(/^/gm, ' ')}\n` +
` })().catch((error) => trace('timer handler failed: ' + error + '\\n'))\n` +
`}, ${interval})\n`
)
}
forBlock['stackchan_set_emotion'] = (block) => {
return `robot.face.setEmotion(Emotion.${block.getFieldValue('EMOTION')})\n`
}
forBlock['stackchan_set_color'] = (block) => {
const key = block.getFieldValue('KEY')
const color = block.getFieldValue('COLOR')
return `robot.face.setColor('${key}', ...hexToRgb('${color}'))\n`
}
forBlock['stackchan_set_mouth'] = (block) => {
return `robot.face.setMouthOpen(${Number(block.getFieldValue('VALUE'))})\n`
}
forBlock['stackchan_say'] = (block, gen) => {
const text = gen.valueToCode(block, 'TEXT', Order.NONE) || "''"
return `await robot.audio.say(String(${text}))\n`
}
forBlock['stackchan_show_balloon'] = (block, gen) => {
const text = gen.valueToCode(block, 'TEXT', Order.NONE) || "''"
return `robot.ui.showBalloon(String(${text}))\n`
}
forBlock['stackchan_hide_balloon'] = () => `robot.ui.hideBalloon()\n`
forBlock['stackchan_tone'] = (block) => {
const note = Number(block.getFieldValue('NOTE'))
const duration = Number(block.getFieldValue('DURATION'))
return `await robot.audio.tone(${note}, ${duration})\n`
}
forBlock['stackchan_look_at'] = (block) => {
const x = Number(block.getFieldValue('X'))
const y = Number(block.getFieldValue('Y'))
const z = Number(block.getFieldValue('Z'))
return `robot.motion.lookAt([${x}, ${y}, ${z}])\n`
}
forBlock['stackchan_look_away'] = () => `robot.motion.lookAway()\n`
forBlock['stackchan_set_torque'] = (block) => {
return `await robot.motion.setTorque(${block.getFieldValue('TORQUE')})\n`
}
forBlock['stackchan_set_pose'] = (block) => {
const pitch = Number(block.getFieldValue('PITCH'))
const yaw = Number(block.getFieldValue('YAW'))
const time = Number(block.getFieldValue('TIME'))
return `await robot.motion.setPose({ rotation: { p: (${pitch} * Math.PI) / 180, y: (${yaw} * Math.PI) / 180, r: 0 } }, ${time})\n`
}
forBlock['stackchan_light_on'] = (block) => {
const name = escapeSingleQuoted(block.getFieldValue('NAME'))
const color = block.getFieldValue('COLOR')
return `robot.lighting.lightOn('${name}', ...hexToRgb('${color}'))\n`
}
forBlock['stackchan_light_off'] = (block) => {
return `robot.lighting.lightOff('${escapeSingleQuoted(block.getFieldValue('NAME'))}')\n`
}
forBlock['stackchan_light_rainbow'] = (block) => {
return `robot.lighting.lightRainbow('${escapeSingleQuoted(block.getFieldValue('NAME'))}')\n`
}
forBlock['stackchan_light_blink'] = (block) => {
const name = escapeSingleQuoted(block.getFieldValue('NAME'))
const color = block.getFieldValue('COLOR')
const interval = Number(block.getFieldValue('INTERVAL'))
return `robot.lighting.lightBlink('${name}', ...hexToRgb('${color}'), ${interval})\n`
}
forBlock['stackchan_drawer_control'] = (block) => {
return `robot.ui.${block.getFieldValue('ACTION')}()\n`
}
forBlock['stackchan_show_face'] = () => `robot.ui.showFace()\n`
forBlock['stackchan_wait'] = (block) => {
return `await wait(${Number(block.getFieldValue('DURATION'))})\n`
}
forBlock['stackchan_trace'] = (block, gen) => {
const text = gen.valueToCode(block, 'TEXT', Order.NONE) || "''"
return `trace(String(${text}) + '\\n')\n`
}
forBlock['stackchan_random'] = (block) => {
const min = Number(block.getFieldValue('MIN'))
const max = Number(block.getFieldValue('MAX'))
return [`randomBetween(${min}, ${max})`, Order.FUNCTION_CALL]
}
}
export const TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'イベント',
colour: `${BLOCK_STYLE.event}`,
contents: [
{ kind: 'block', type: 'stackchan_on_start' },
{ kind: 'block', type: 'stackchan_on_button' },
{ kind: 'block', type: 'stackchan_every' },
{ kind: 'block', type: 'stackchan_on_imu' },
{ kind: 'block', type: 'stackchan_on_touch' },
{ kind: 'block', type: 'stackchan_on_drawer_button' },
],
},
{
kind: 'category',
name: 'かお',
colour: `${BLOCK_STYLE.face}`,
contents: [
{ kind: 'block', type: 'stackchan_set_emotion' },
{ kind: 'block', type: 'stackchan_set_color' },
{ kind: 'block', type: 'stackchan_set_mouth' },
],
},
{
kind: 'category',
name: 'おしゃべり',
colour: `${BLOCK_STYLE.speech}`,
contents: [
{
kind: 'block',
type: 'stackchan_say',
inputs: { TEXT: { shadow: { type: 'text', fields: { TEXT: 'こんにちは' } } } },
},
{
kind: 'block',
type: 'stackchan_show_balloon',
inputs: { TEXT: { shadow: { type: 'text', fields: { TEXT: 'やあ!' } } } },
},
{ kind: 'block', type: 'stackchan_hide_balloon' },
{ kind: 'block', type: 'stackchan_tone' },
],
},
{
kind: 'category',
name: 'うごき',
colour: `${BLOCK_STYLE.motion}`,
contents: [
{ kind: 'block', type: 'stackchan_look_at' },
{ kind: 'block', type: 'stackchan_look_away' },
{ kind: 'block', type: 'stackchan_set_pose' },
{ kind: 'block', type: 'stackchan_set_torque' },
],
},
{
kind: 'category',
name: 'LED',
colour: `${BLOCK_STYLE.light}`,
contents: [
{ kind: 'block', type: 'stackchan_light_on' },
{ kind: 'block', type: 'stackchan_light_off' },
{ kind: 'block', type: 'stackchan_light_rainbow' },
{ kind: 'block', type: 'stackchan_light_blink' },
],
},
{
kind: 'category',
name: 'がめん',
colour: `${BLOCK_STYLE.ui}`,
contents: [
{ kind: 'block', type: 'stackchan_drawer_control' },
{ kind: 'block', type: 'stackchan_show_face' },
],
},
{
kind: 'category',
name: 'どうぐ',
colour: `${BLOCK_STYLE.util}`,
contents: [
{ kind: 'block', type: 'stackchan_wait' },
{
kind: 'block',
type: 'stackchan_trace',
inputs: { TEXT: { shadow: { type: 'text', fields: { TEXT: 'debug' } } } },
},
{ kind: 'block', type: 'stackchan_random' },
],
},
{ kind: 'sep' },
{
kind: 'category',
name: 'ロジック',
categorystyle: 'logic_category',
contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
],
},
{
kind: 'category',
name: 'くり返し',
categorystyle: 'loop_category',
contents: [
{
kind: 'block',
type: 'controls_repeat_ext',
inputs: { TIMES: { shadow: { type: 'math_number', fields: { NUM: 3 } } } },
},
{ kind: 'block', type: 'controls_whileUntil' },
],
},
{
kind: 'category',
name: '数',
categorystyle: 'math_category',
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_round' },
],
},
{
kind: 'category',
name: 'テキスト',
categorystyle: 'text_category',
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
],
},
{ kind: 'category', name: '変数', categorystyle: 'variable_category', custom: 'VARIABLE' },
],
}
/**
* Generate the complete mod.js source from a Blockly workspace.
*/
export function generateModSource(generator, workspace) {
const body = generator.workspaceToCode(workspace)
return assembleModSource(body.replace(/\s+$/, ''))
}