From 37a9000b0773f60595a6a88378ddeb7501bf9702 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 12 Sep 2025 14:46:39 -0400 Subject: [PATCH 01/34] For rough prototype --- src/blocks/mrc_steps.ts | 71 +++++++++++++++++++++++++++++++ src/blocks/setup_custom_blocks.ts | 2 + src/toolbox/test_category.ts | 8 +--- 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/blocks/mrc_steps.ts diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts new file mode 100644 index 00000000..3d5fb0db --- /dev/null +++ b/src/blocks/mrc_steps.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Blocks for class method definition + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import { MRC_STYLE_FUNCTIONS } from '../themes/styles'; +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; + +export const BLOCK_NAME = 'mrc_steps'; +const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; + + +export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg; +interface StepsMixin extends StepsMixinType { +} +type StepsMixinType = typeof STEPS; + +const STEPS = { + /** + * Block initialization. + */ + init: function (this: StepsBlock): void { + this.appendDummyInput() + .appendField(createFieldNonEditableText('steps')); + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput('Step 0')); + this.appendStatementInput('STEP_0'); + this.appendValueInput('CONDITION_0') + .setCheck('Boolean') + .appendField('Advance when'); + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput('Step 1')); + this.appendStatementInput('STEP_1'); + this.appendValueInput('CONDITION_1') + .setCheck('Boolean') + .appendField('Finish when'); + this.setInputsInline(false); + this.setStyle(MRC_STYLE_FUNCTIONS); + this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); + + } +}; + +export const setup = function () { + Blockly.Blocks[BLOCK_NAME] = STEPS; +}; + +export const pythonFromBlock = function ( + _block: StepsBlock, + _generator: ExtendedPythonGenerator, +) { + return 'def steps(self):\n pass' +} \ No newline at end of file diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index 352182f3..aaa7ace8 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -18,6 +18,7 @@ import * as OpModeDetails from './mrc_opmode_details'; import * as ParameterMutator from './mrc_param_container' import * as Port from './mrc_port'; import * as SetPythonVariable from './mrc_set_python_variable'; +import * as Steps from './mrc_steps'; const customBlocks = [ CallPythonFunction, @@ -39,6 +40,7 @@ const customBlocks = [ ParameterMutator, Port, SetPythonVariable, + Steps, ]; export const setup = function(forBlock: any) { diff --git a/src/toolbox/test_category.ts b/src/toolbox/test_category.ts index 04bc1701..594416fb 100644 --- a/src/toolbox/test_category.ts +++ b/src/toolbox/test_category.ts @@ -22,13 +22,7 @@ export function getCategory(): toolboxItems.Category { contents.push({ kind: 'block', - type: 'mrc_port', - extraState: { - ports: [ - { portType: 'USB Port', portNumber: 1 }, - { portType: 'Expansion Hub Motor Port', portNumber: 2 }, - ], - }, + type: 'mrc_steps', }); return { From 5e3ae9a974cf193abfc0b5d7038169d647286787 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 15 Sep 2025 11:12:20 -0400 Subject: [PATCH 02/34] Split step into a label and field --- src/blocks/mrc_steps.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 3d5fb0db..9e85f53e 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -41,13 +41,15 @@ const STEPS = { this.appendDummyInput() .appendField(createFieldNonEditableText('steps')); this.appendDummyInput() - .appendField(new Blockly.FieldTextInput('Step 0')); + .appendField('Step') + .appendField(new Blockly.FieldTextInput('0')); this.appendStatementInput('STEP_0'); this.appendValueInput('CONDITION_0') .setCheck('Boolean') .appendField('Advance when'); this.appendDummyInput() - .appendField(new Blockly.FieldTextInput('Step 1')); + .appendField('Step') + .appendField(new Blockly.FieldTextInput('1')); this.appendStatementInput('STEP_1'); this.appendValueInput('CONDITION_1') .setCheck('Boolean') From 28d38aebf73fd00e59e0695a5e7418f747f437bf Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 10 Oct 2025 18:10:58 -0400 Subject: [PATCH 03/34] Change to allow field flydown for more than params --- src/blocks/mrc_class_method_def.ts | 4 +- src/blocks/mrc_event_handler.ts | 4 +- src/fields/field_flydown.ts | 66 +++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 0c6af826..ad0e220c 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -22,7 +22,7 @@ import * as Blockly from 'blockly'; import { MRC_STYLE_FUNCTIONS } from '../themes/styles'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText' -import { createFieldFlydown } from '../fields/field_flydown'; +import { createParameterFieldFlydown } from '../fields/field_flydown'; import { Order } from 'blockly/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import * as storageModule from '../storage/module'; @@ -241,7 +241,7 @@ const CLASS_METHOD_DEF = { this.removeParameterFields(input); this.mrcParameters.forEach((param) => { const paramName = FIELD_PARAM_PREFIX + param.name; - input.appendField(createFieldFlydown(param.name, false), paramName); + input.appendField(createParameterFieldFlydown(param.name, false), paramName); }); } } diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 3bb0aac2..18c121fe 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -27,7 +27,7 @@ import type { MessageInstance } from 'antd/es/message/interface'; import { Parameter } from './mrc_class_method_def'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createFieldFlydown } from '../fields/field_flydown'; +import { createParameterFieldFlydown } from '../fields/field_flydown'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles'; import * as toolboxItems from '../toolbox/items'; @@ -146,7 +146,7 @@ const EVENT_HANDLER = { this.removeParameterFields(input); this.mrcParameters.forEach((param) => { const paramName = `PARAM_${param.name}`; - input.appendField(createFieldFlydown(param.name, false), paramName); + input.appendField(createParameterFieldFlydown(param.name, false), paramName); }); } } else { diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index cf75e2ff..5f9392df 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -179,10 +179,17 @@ export class FieldFlydown extends Blockly.FieldTextInput { private boundMouseOutHandler_: (e: Event) => void; private showTimeout_: number | null = null; private hideTimeout_: number | null = null; - - constructor(value: string, isEditable: boolean, displayLocation: FlydownLocation = FlydownLocation.DISPLAY_RIGHT) { + private createFlyoutDefinition_: (text: string) => Blockly.utils.toolbox.FlyoutDefinition; + + constructor( + value: string, + isEditable: boolean, + createFlyoutDefinition: (text: string) => Blockly.utils.toolbox.FlyoutDefinition, + displayLocation: FlydownLocation = FlydownLocation.DISPLAY_RIGHT + ) { super(value); this.EDITABLE = isEditable; + this.createFlyoutDefinition_ = createFlyoutDefinition; this.displayLocation_ = displayLocation; // Bind the handlers @@ -309,17 +316,7 @@ export class FieldFlydown extends Blockly.FieldTextInput { } private getBlocksForFlydown_() { const name = this.getText(); - return { - contents: [ - { - kind: 'block', - type: 'mrc_get_parameter', - fields: { - PARAMETER_NAME: name, - }, - }, - ] - }; + return this.createFlyoutDefinition_(name); } @@ -372,9 +369,48 @@ export class FieldFlydown extends Blockly.FieldTextInput { } super.dispose(); } +} + +export function createParameterBlock(paramName: string): Blockly.utils.toolbox.FlyoutDefinition { + return { + contents: [ + { + kind: 'block', + type: 'mrc_get_parameter', + fields: { + PARAMETER_NAME: paramName, + }, + }, + ] + }; +} + +export function createAdvanceToBlock(stepName: string): Blockly.utils.toolbox.FlyoutDefinition { + return { + contents: [ + { + kind: 'block', + type: 'mrc_advance_to_step', + fields: { + STEP_NAME: stepName, + }, + }, + ] + }; +} + +export function createFieldFlydown( + label: string, + isEditable: boolean, + createFlyoutDefinition: (text: string) => Blockly.utils.toolbox.FlyoutDefinition +): Blockly.Field { + return new FieldFlydown(label, isEditable, createFlyoutDefinition); +} +export function createParameterFieldFlydown(paramName: string, isEditable: boolean): Blockly.Field { + return new FieldFlydown(paramName, isEditable, createParameterBlock); } -export function createFieldFlydown(label: string, isEditable: boolean): Blockly.Field { - return new FieldFlydown(label, isEditable); +export function createStepFieldFlydown(paramName: string, isEditable: boolean): Blockly.Field { + return new FieldFlydown(paramName, isEditable, createAdvanceToBlock); } \ No newline at end of file From bf260847a08fa5ccc1fb739eb4a1199c4f95e0dd Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 10 Oct 2025 18:11:27 -0400 Subject: [PATCH 04/34] Add advance to step blocks --- src/blocks/mrc_advance_to_step.ts | 68 +++++++++++++++++++++++++++++++ src/blocks/setup_custom_blocks.ts | 2 + 2 files changed, 70 insertions(+) create mode 100644 src/blocks/mrc_advance_to_step.ts diff --git a/src/blocks/mrc_advance_to_step.ts b/src/blocks/mrc_advance_to_step.ts new file mode 100644 index 00000000..31decc08 --- /dev/null +++ b/src/blocks/mrc_advance_to_step.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview This is a block that allows your code to use a parameter + * that is passed to a method. + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import {Order} from 'blockly/python'; + +import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; +import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; +import {MRC_STYLE_VARIABLES} from '../themes/styles'; + +export const BLOCK_NAME = 'mrc_advance_to_step'; + +const FIELD_STEP_NAME = 'STEP_NAME'; + +type AdvanceToStepBlock = Blockly.Block & Blockly.BlockSvg & AdvanceToStepMixin; + +interface AdvanceToStepMixin extends AdvanceToStepMixinType { +} + +type AdvanceToStepMixinType = typeof ADVANCE_TO_STEP_BLOCK; + +const ADVANCE_TO_STEP_BLOCK = { + /** + * Block initialization. + */ + init: function(this: AdvanceToStepBlock): void { + this.appendDummyInput() + .appendField('Advance to step') + .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); + this.setPreviousStatement(true, null); + this.setInputsInline(true); + this.setStyle(MRC_STYLE_VARIABLES); + this.setTooltip('Advance to the specified step when the condition is true.'); + }, +}; + +export const setup = function() { + Blockly.Blocks[BLOCK_NAME] = ADVANCE_TO_STEP_BLOCK; +}; + +export const pythonFromBlock = function( + block: AdvanceToStepBlock, + _generator: ExtendedPythonGenerator, +) { + // TODO (Alan) : Specify the type here as well + const code = '# TODO: Advance to step ' + block.getFieldValue(FIELD_STEP_NAME) + '\n'; + + return [code, Order.ATOMIC]; +}; diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index f28f3c1c..e04fa89d 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -19,6 +19,7 @@ import * as ParamContainer from './mrc_param_container' import * as Port from './mrc_port'; import * as SetPythonVariable from './mrc_set_python_variable'; import * as Steps from './mrc_steps'; +import * as AdvanceToStep from './mrc_advance_to_step'; const customBlocks = [ CallPythonFunction, @@ -41,6 +42,7 @@ const customBlocks = [ Port, SetPythonVariable, Steps, + AdvanceToStep ]; export const setup = function(forBlock: any) { From 4afcbfffd530fa386c589dade52bfe708b494fa6 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 10 Oct 2025 18:11:40 -0400 Subject: [PATCH 05/34] use flydown --- src/blocks/mrc_steps.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 9e85f53e..33996ddf 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -23,6 +23,8 @@ import * as Blockly from 'blockly'; import { MRC_STYLE_FUNCTIONS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; +import { createStepFieldFlydown } from '../fields/field_flydown'; +import * as paramContainer from './mrc_param_container' export const BLOCK_NAME = 'mrc_steps'; const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; @@ -42,26 +44,43 @@ const STEPS = { .appendField(createFieldNonEditableText('steps')); this.appendDummyInput() .appendField('Step') - .appendField(new Blockly.FieldTextInput('0')); + .appendField(createStepFieldFlydown('0', false)); this.appendStatementInput('STEP_0'); this.appendValueInput('CONDITION_0') .setCheck('Boolean') .appendField('Advance when'); this.appendDummyInput() .appendField('Step') - .appendField(new Blockly.FieldTextInput('1')); + .appendField(createStepFieldFlydown('1', false)); this.appendStatementInput('STEP_1'); this.appendValueInput('CONDITION_1') .setCheck('Boolean') .appendField('Finish when'); this.setInputsInline(false); this.setStyle(MRC_STYLE_FUNCTIONS); - this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); - - } + this.setMutator(paramContainer.getMutatorIcon(this)); + }, + compose: function (this: StepsBlock, containerBlock: Blockly.Block) { + if (containerBlock.type !== paramContainer.PARAM_CONTAINER_BLOCK_NAME) { + throw new Error('compose: containerBlock.type should be ' + paramContainer.PARAM_CONTAINER_BLOCK_NAME); + } + const paramContainerBlock = containerBlock as paramContainer.ParamContainerBlock; + const paramItemBlocks: paramContainer.ParamItemBlock[] = paramContainerBlock.getParamItemBlocks(); + + }, + decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { + const parameterNames: string[] = []; + + return paramContainer.createMutatorBlocks(workspace, parameterNames); + }, +}; + +const MUTATOR_STEPS = { + }; export const setup = function () { + Blockly.Blocks[MUTATOR_BLOCK_NAME] = MUTATOR_STEPS; Blockly.Blocks[BLOCK_NAME] = STEPS; }; From 200b773ddc321d81cdb72fc06cc503601dd13d9b Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 24 Oct 2025 20:08:15 -0400 Subject: [PATCH 06/34] First bit of having steps --- src/blocks/mrc_step_container.ts | 212 +++++++++++++++++++++++++++ src/blocks/mrc_steps.ts | 99 ++++++++----- src/blocks/setup_custom_blocks.ts | 2 + src/blocks/tokens.ts | 2 + src/i18n/locales/en/translation.json | 2 + src/i18n/locales/es/translation.json | 2 + src/themes/styles.ts | 4 + 7 files changed, 287 insertions(+), 36 deletions(-) create mode 100644 src/blocks/mrc_step_container.ts diff --git a/src/blocks/mrc_step_container.ts b/src/blocks/mrc_step_container.ts new file mode 100644 index 00000000..1e11ed64 --- /dev/null +++ b/src/blocks/mrc_step_container.ts @@ -0,0 +1,212 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Mutator for steps. + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; + +export const STEP_CONTAINER_BLOCK_NAME = 'mrc_step_container'; +const STEP_ITEM_BLOCK_NAME = 'mrc_step_item'; + +export const setup = function () { + Blockly.Blocks[STEP_CONTAINER_BLOCK_NAME] = STEP_CONTAINER; + Blockly.Blocks[STEP_ITEM_BLOCK_NAME] = STEP_ITEM; +}; + +// The step container block. + +const INPUT_STACK = 'STACK'; + +export type StepContainerBlock = StepContainerMixin & Blockly.BlockSvg; +interface StepContainerMixin extends StepContainerMixinType {} +type StepContainerMixinType = typeof STEP_CONTAINER; + +const STEP_CONTAINER = { + init: function (this: StepContainerBlock) { + this.appendDummyInput().appendField(Blockly.Msg.STEPS); + this.appendStatementInput(INPUT_STACK); + this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.contextMenu = false; + }, + getStepItemBlocks: function (this: StepContainerBlock): StepItemBlock[] { + const stepItemBlocks: StepItemBlock[] = []; + let block = this.getInputTargetBlock(INPUT_STACK); + while (block && !block.isInsertionMarker()) { + if (block.type !== STEP_ITEM_BLOCK_NAME) { + throw new Error('getItemNames: block.type should be ' + STEP_ITEM_BLOCK_NAME); + } + stepItemBlocks.push(block as StepItemBlock); + block = block.nextConnection && block.nextConnection.targetBlock(); + } + return stepItemBlocks; + }, +}; + +// The step item block. + +const FIELD_NAME = 'NAME'; + +export type StepItemBlock = StepItemMixin & Blockly.BlockSvg; +interface StepItemMixin extends StepItemMixinType { + originalName: string, +} + +type StepItemMixinType = typeof STEP_ITEM; + +const STEP_ITEM = { + init: function (this: StepItemBlock) { + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput(''), FIELD_NAME); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.originalName = ''; + this.contextMenu = false; + }, + makeNameLegal: function (this: StepItemBlock): void { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock) { + const otherNames: string[] = [] + rootBlock!.getDescendants(true)?.forEach(itemBlock => { + if (itemBlock != this) { + otherNames.push(itemBlock.getFieldValue(FIELD_NAME)); + } + }); + let currentName = this.getFieldValue(FIELD_NAME); + while (otherNames.includes(currentName)) { + // Check if currentName ends with a number + const match = currentName.match(/^(.*?)(\d+)$/); + if (match) { + // If it ends with a number, increment it + const baseName = match[1]; + const number = parseInt(match[2], 10); + currentName = baseName + (number + 1); + } else { + // If it doesn't end with a number, append 2 + currentName = currentName + '2'; + } + } + this.setFieldValue(currentName, FIELD_NAME); + updateMutatorFlyout(this.workspace); + } + }, + getName: function (this: StepItemBlock): string { + return this.getFieldValue(FIELD_NAME); + }, + getOriginalName: function (this: StepItemBlock): string { + return this.originalName; + }, + setOriginalName: function (this: StepItemBlock, originalName: string): void { + this.originalName = originalName; + }, +} + +/** + * Updates the mutator's flyout so that it contains a single step item block + * whose name is not a duplicate of an existing step item. + * + * @param workspace The mutator's workspace. This workspace's flyout is what is being updated. + */ +function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { + const usedNames: string[] = []; + workspace.getBlocksByType(STEP_ITEM_BLOCK_NAME, false).forEach(block => { + usedNames.push(block.getFieldValue(FIELD_NAME)); + }); + + // Find the first unused number starting from 0 + let counter = 0; + let uniqueName = counter.toString(); + while (usedNames.includes(uniqueName)) { + counter++; + uniqueName = counter.toString(); + } + + const jsonBlock = { + kind: 'block', + type: STEP_ITEM_BLOCK_NAME, + fields: { + NAME: uniqueName, + }, + }; + + workspace.updateToolbox({ contents: [jsonBlock] }); +} + +/** + * The blockly event listener function for the mutator's workspace. + */ +function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abstract) { + if (event.type === Blockly.Events.BLOCK_MOVE) { + const blockMoveEvent = event as Blockly.Events.BlockMove; + const reason: string[] = blockMoveEvent.reason ?? []; + if (reason.includes('connect') && blockMoveEvent.blockId) { + const block = mutatorWorkspace.getBlockById(blockMoveEvent.blockId); + if (block && block.type === STEP_ITEM_BLOCK_NAME) { + (block as StepItemBlock).makeNameLegal(); + } + } + } else if (event.type === Blockly.Events.BLOCK_CHANGE) { + const blockChangeEvent = event as Blockly.Events.BlockChange; + if (blockChangeEvent.blockId) { + const block = mutatorWorkspace.getBlockById(blockChangeEvent.blockId); + if (block && block.type === STEP_ITEM_BLOCK_NAME) { + (block as StepItemBlock).makeNameLegal(); + } + } + } +} + +/** + * Called for mrc_event and mrc_class_method_def blocks when their mutator opesn. + * Triggers a flyout update and adds an event listener to the mutator workspace. + * + * @param block The block whose mutator is open. + */ +export function onMutatorOpen(block: Blockly.BlockSvg) { + const mutatorIcon = block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon; + const mutatorWorkspace = mutatorIcon.getWorkspace()!; + updateMutatorFlyout(mutatorWorkspace); + mutatorWorkspace.addChangeListener(event => onChange(mutatorWorkspace, event)); +} + +/** + * Returns the MutatorIcon for the given block. + */ +export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIcon { + return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block); +} + +export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg { + // First create the container block. + const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg; + containerBlock.initSvg(); + + // Then add one step item block for each step. + let connection = containerBlock!.getInput(INPUT_STACK)!.connection; + for (const stepName of stepNames) { + const itemBlock = workspace.newBlock(STEP_ITEM_BLOCK_NAME) as StepItemBlock; + itemBlock.initSvg(); + itemBlock.setFieldValue(stepName, FIELD_NAME); + itemBlock.originalName = stepName; + connection!.connect(itemBlock.previousConnection!); + connection = itemBlock.nextConnection; + } + return containerBlock; +} diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 33996ddf..da6c390c 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -20,18 +20,18 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import { MRC_STYLE_FUNCTIONS } from '../themes/styles'; +import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { createStepFieldFlydown } from '../fields/field_flydown'; -import * as paramContainer from './mrc_param_container' +import * as stepContainer from './mrc_step_container' export const BLOCK_NAME = 'mrc_steps'; -const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; +// const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg; interface StepsMixin extends StepsMixinType { + mrcStepNames: string[]; } type StepsMixinType = typeof STEPS; @@ -40,47 +40,74 @@ const STEPS = { * Block initialization. */ init: function (this: StepsBlock): void { + this.mrcStepNames = []; this.appendDummyInput() - .appendField(createFieldNonEditableText('steps')); - this.appendDummyInput() - .appendField('Step') - .appendField(createStepFieldFlydown('0', false)); - this.appendStatementInput('STEP_0'); - this.appendValueInput('CONDITION_0') - .setCheck('Boolean') - .appendField('Advance when'); - this.appendDummyInput() - .appendField('Step') - .appendField(createStepFieldFlydown('1', false)); - this.appendStatementInput('STEP_1'); - this.appendValueInput('CONDITION_1') - .setCheck('Boolean') - .appendField('Finish when'); + .appendField(Blockly.Msg.STEPS); + /* + this.appendValueInput('CONDITION_0') + .appendField(createStepFieldFlydown('shoot', true)) + .setCheck('Boolean') + .appendField('Repeat Until'); + this.appendStatementInput('STEP_0'); + + this.appendValueInput('CONDITION_1') + .appendField(createStepFieldFlydown('move', true)) + .setCheck('Boolean') + .appendField('Repeat Until'); + this.appendStatementInput('STEP_1'); + */ this.setInputsInline(false); - this.setStyle(MRC_STYLE_FUNCTIONS); - this.setMutator(paramContainer.getMutatorIcon(this)); - }, + this.setStyle(MRC_STYLE_STEPS); + this.setMutator(stepContainer.getMutatorIcon(this)); + }, compose: function (this: StepsBlock, containerBlock: Blockly.Block) { - if (containerBlock.type !== paramContainer.PARAM_CONTAINER_BLOCK_NAME) { - throw new Error('compose: containerBlock.type should be ' + paramContainer.PARAM_CONTAINER_BLOCK_NAME); - } - const paramContainerBlock = containerBlock as paramContainer.ParamContainerBlock; - const paramItemBlocks: paramContainer.ParamItemBlock[] = paramContainerBlock.getParamItemBlocks(); - + if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { + throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); + } + const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; + const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); + stepItemBlocks.forEach((stepItemBlock) => { + }); + this.mrcStepNames = []; + stepItemBlocks.forEach((stepItemBlock) => { + this.mrcStepNames.push(stepItemBlock.getName()); + }); + // TODO: Update any jump blocks to have the correct name + this.updateShape_(); }, decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { - const parameterNames: string[] = []; - - return paramContainer.createMutatorBlocks(workspace, parameterNames); + const stepNames: string[] = []; + this.mrcStepNames.forEach(step => { + stepNames.push(step); + }); + return stepContainer.createMutatorBlocks(workspace, stepNames); + }, + /** + * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + */ + mrcOnMutatorOpen: function(this: StepsBlock): void { + stepContainer.onMutatorOpen(this); + }, + updateShape_: function (this: StepsBlock): void { + // some way of knowing what was there before and what is there now + let success = true; + let i = 0; + while (success){ + success = this.removeInput('CONDITION_' + i, true); + success = this.removeInput('STEP_' + i, true); + i++; + } + for (let j = 0; j < this.mrcStepNames.length; j++) { + this.appendValueInput('CONDITION_' + j) + .appendField(createStepFieldFlydown(this.mrcStepNames[j], true)) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput('STEP_' + j); + } }, -}; - -const MUTATOR_STEPS = { - }; export const setup = function () { - Blockly.Blocks[MUTATOR_BLOCK_NAME] = MUTATOR_STEPS; Blockly.Blocks[BLOCK_NAME] = STEPS; }; diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index e04fa89d..e959a89b 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -19,6 +19,7 @@ import * as ParamContainer from './mrc_param_container' import * as Port from './mrc_port'; import * as SetPythonVariable from './mrc_set_python_variable'; import * as Steps from './mrc_steps'; +import * as StepContainer from './mrc_step_container'; import * as AdvanceToStep from './mrc_advance_to_step'; const customBlocks = [ @@ -42,6 +43,7 @@ const customBlocks = [ Port, SetPythonVariable, Steps, + StepContainer, AdvanceToStep ]; diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 3656664a..8b817da0 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -129,6 +129,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { MORE_MECHANISM_METHODS_LABEL: t('BLOCKLY.MORE_MECHANISM_METHODS_LABEL'), MORE_OPMODE_METHODS_LABEL: t('BLOCKLY.MORE_OPMODE_METHODS_LABEL'), COMMENT_DEFAULT_TEXT: t('BLOCKLY.COMMENT_DEFAULT_TEXT'), + STEPS: t('BLOCKLY.STEPS'), + REPEAT_UNTIL: t('BLOCKLY.REPEAT_UNTIL'), } }; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 57ba9688..29fa0351 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -146,6 +146,8 @@ "GET": "get", "SET": "set", "TO": "to", + "STEPS": "steps", + "REPEAT_UNTIL": "Repeat Until", "CUSTOM_EVENTS_LABEL": "Custom Events", "CUSTOM_METHODS_LABEL": "Custom Methods", "MORE_ROBOT_METHODS_LABEL": "More Robot Methods", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index c3739b1c..897ae389 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -147,6 +147,8 @@ "GET": "obtener", "SET": "establecer", "TO": "a", + "STEPS": "pasos", + "REPEAT_UNTIL": "Repetir Hasta", "CUSTOM_EVENTS_LABEL": "Eventos Personalizados", "CUSTOM_METHODS_LABEL": "Métodos Personalizados", "MORE_ROBOT_METHODS_LABEL": "Más Métodos del Robot", diff --git a/src/themes/styles.ts b/src/themes/styles.ts index c8beecd4..28377a40 100644 --- a/src/themes/styles.ts +++ b/src/themes/styles.ts @@ -22,6 +22,7 @@ import * as Blockly from 'blockly/core' export const MRC_STYLE_FUNCTIONS = 'mrc_style_function'; +export const MRC_STYLE_STEPS = 'mrc_style_steps'; export const MRC_STYLE_ENUM = 'mrc_style_enum'; export const MRC_STYLE_VARIABLES = 'mrc_style_variables'; export const MRC_STYLE_COMMENTS = 'mrc_style_comments'; @@ -51,6 +52,9 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { theme.setBlockStyle(MRC_STYLE_EVENTS, { ...procedureStyle }); + theme.setBlockStyle(MRC_STYLE_STEPS, { + ...procedureStyle, + }); theme.setBlockStyle(MRC_STYLE_ENUM, { ...variableStyle }); From 7ebcb0012ed5e82b42ea8110c496258b254ea4dd Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 24 Oct 2025 20:10:09 -0400 Subject: [PATCH 07/34] Make default starting out with one step --- src/blocks/mrc_steps.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index da6c390c..7524fb05 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -40,7 +40,7 @@ const STEPS = { * Block initialization. */ init: function (this: StepsBlock): void { - this.mrcStepNames = []; + this.mrcStepNames = ["0"]; this.appendDummyInput() .appendField(Blockly.Msg.STEPS); /* @@ -59,6 +59,7 @@ const STEPS = { this.setInputsInline(false); this.setStyle(MRC_STYLE_STEPS); this.setMutator(stepContainer.getMutatorIcon(this)); + this.updateShape_(); }, compose: function (this: StepsBlock, containerBlock: Blockly.Block) { if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { @@ -85,9 +86,9 @@ const STEPS = { /** * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. */ - mrcOnMutatorOpen: function(this: StepsBlock): void { + mrcOnMutatorOpen: function(this: StepsBlock): void { stepContainer.onMutatorOpen(this); - }, + }, updateShape_: function (this: StepsBlock): void { // some way of knowing what was there before and what is there now let success = true; From 06ebe8e56f488ef2902d8658997e35db9ccea3cf Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 24 Oct 2025 20:21:00 -0400 Subject: [PATCH 08/34] Change from Advance to Jump --- ...advance_to_step.ts => mrc_jump_to_step.ts} | 25 +++++++++---------- src/blocks/mrc_steps.ts | 13 ---------- src/blocks/setup_custom_blocks.ts | 4 +-- src/fields/field_flydown.ts | 6 +++-- 4 files changed, 18 insertions(+), 30 deletions(-) rename src/blocks/{mrc_advance_to_step.ts => mrc_jump_to_step.ts} (66%) diff --git a/src/blocks/mrc_advance_to_step.ts b/src/blocks/mrc_jump_to_step.ts similarity index 66% rename from src/blocks/mrc_advance_to_step.ts rename to src/blocks/mrc_jump_to_step.ts index 31decc08..2b377e68 100644 --- a/src/blocks/mrc_advance_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -16,8 +16,7 @@ */ /** - * @fileoverview This is a block that allows your code to use a parameter - * that is passed to a method. + * @fileoverview This is a block that allows your code to jump to a specific step. * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; @@ -27,42 +26,42 @@ import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; import {MRC_STYLE_VARIABLES} from '../themes/styles'; -export const BLOCK_NAME = 'mrc_advance_to_step'; +export const BLOCK_NAME = 'mrc_jump_to_step'; const FIELD_STEP_NAME = 'STEP_NAME'; -type AdvanceToStepBlock = Blockly.Block & Blockly.BlockSvg & AdvanceToStepMixin; +type JumpToStepBlock = Blockly.Block & Blockly.BlockSvg & JumpToStepMixin; -interface AdvanceToStepMixin extends AdvanceToStepMixinType { +interface JumpToStepMixin extends JumpToStepMixinType { } -type AdvanceToStepMixinType = typeof ADVANCE_TO_STEP_BLOCK; +type JumpToStepMixinType = typeof JUMP_TO_STEP_BLOCK; -const ADVANCE_TO_STEP_BLOCK = { +const JUMP_TO_STEP_BLOCK = { /** * Block initialization. */ - init: function(this: AdvanceToStepBlock): void { + init: function(this: JumpToStepBlock): void { this.appendDummyInput() - .appendField('Advance to step') + .appendField('Jump to') .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); this.setPreviousStatement(true, null); this.setInputsInline(true); this.setStyle(MRC_STYLE_VARIABLES); - this.setTooltip('Advance to the specified step when the condition is true.'); + this.setTooltip('Jump to the specified step.'); }, }; export const setup = function() { - Blockly.Blocks[BLOCK_NAME] = ADVANCE_TO_STEP_BLOCK; + Blockly.Blocks[BLOCK_NAME] = JUMP_TO_STEP_BLOCK; }; export const pythonFromBlock = function( - block: AdvanceToStepBlock, + block: JumpToStepBlock, _generator: ExtendedPythonGenerator, ) { // TODO (Alan) : Specify the type here as well - const code = '# TODO: Advance to step ' + block.getFieldValue(FIELD_STEP_NAME) + '\n'; + const code = '# TODO: Jump to step ' + block.getFieldValue(FIELD_STEP_NAME) + '\n'; return [code, Order.ATOMIC]; }; diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 7524fb05..cb702e42 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -43,19 +43,6 @@ const STEPS = { this.mrcStepNames = ["0"]; this.appendDummyInput() .appendField(Blockly.Msg.STEPS); - /* - this.appendValueInput('CONDITION_0') - .appendField(createStepFieldFlydown('shoot', true)) - .setCheck('Boolean') - .appendField('Repeat Until'); - this.appendStatementInput('STEP_0'); - - this.appendValueInput('CONDITION_1') - .appendField(createStepFieldFlydown('move', true)) - .setCheck('Boolean') - .appendField('Repeat Until'); - this.appendStatementInput('STEP_1'); - */ this.setInputsInline(false); this.setStyle(MRC_STYLE_STEPS); this.setMutator(stepContainer.getMutatorIcon(this)); diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index e959a89b..fecdb7bc 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -20,7 +20,7 @@ import * as Port from './mrc_port'; import * as SetPythonVariable from './mrc_set_python_variable'; import * as Steps from './mrc_steps'; import * as StepContainer from './mrc_step_container'; -import * as AdvanceToStep from './mrc_advance_to_step'; +import * as JumpToStep from './mrc_jump_to_step'; const customBlocks = [ CallPythonFunction, @@ -44,7 +44,7 @@ const customBlocks = [ SetPythonVariable, Steps, StepContainer, - AdvanceToStep + JumpToStep ]; export const setup = function(forBlock: any) { diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index 5f9392df..5b2dcac8 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -23,6 +23,8 @@ * https://github.com/mit-cml/blockly-plugins/blob/main/block-lexical-variables/src/fields/field_flydown.js */ import * as Blockly from 'blockly'; +import {BLOCK_NAME as JUMP_TO_STEP} from '../blocks/mrc_jump_to_step'; +import {BLOCK_NAME as GET_PARAMETER} from '../blocks/mrc_get_parameter'; enum FlydownLocation { DISPLAY_BELOW = 'displayBelow', @@ -376,7 +378,7 @@ export function createParameterBlock(paramName: string): Blockly.utils.toolbox.F contents: [ { kind: 'block', - type: 'mrc_get_parameter', + type: GET_PARAMETER, fields: { PARAMETER_NAME: paramName, }, @@ -390,7 +392,7 @@ export function createAdvanceToBlock(stepName: string): Blockly.utils.toolbox.Fl contents: [ { kind: 'block', - type: 'mrc_advance_to_step', + type: JUMP_TO_STEP, fields: { STEP_NAME: stepName, }, From 428f6dfbc1bff2a5734a2bb79306b977c71141ad Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 24 Oct 2025 21:04:26 -0400 Subject: [PATCH 09/34] beginning of generating code --- src/blocks/mrc_steps.ts | 42 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index cb702e42..3ead4aed 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -20,6 +20,8 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; +import {Order} from 'blockly/python'; + import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; @@ -75,6 +77,9 @@ const STEPS = { */ mrcOnMutatorOpen: function(this: StepsBlock): void { stepContainer.onMutatorOpen(this); + }, + mrcOnChange: function(this: StepsBlock): void { + }, updateShape_: function (this: StepsBlock): void { // some way of knowing what was there before and what is there now @@ -100,8 +105,39 @@ export const setup = function () { }; export const pythonFromBlock = function ( - _block: StepsBlock, - _generator: ExtendedPythonGenerator, + block: StepsBlock, + generator: ExtendedPythonGenerator, ) { - return 'def steps(self):\n pass' + + let code = 'def initialize_steps(self):\n'; + code += generator.INDENT + 'self.step_from_name = {}\n'; + code += generator.INDENT + 'self.name_from_step = {}\n'; + block.mrcStepNames.forEach((stepName, index) => { + code += generator.INDENT + `self.step_from_name['${stepName}'] = ${index}\n`; + code += generator.INDENT + `self.name_from_step[${index}] = '${stepName}'\n`; + }); + + code += generator.INDENT + 'self.current_step_index = 0\n'; + code += generator.INDENT + 'self.initialized = True\n'; + + generator.addClassMethodDefinition('initialize_steps', code); + + code = 'def steps(self):\n'; + code += generator.INDENT + 'if not self.initialized:\n'; + code += generator.INDENT.repeat(2) + 'self.initialize_steps()\n\n'; + code += generator.INDENT + 'match self.current_step_index:\n'; + block.mrcStepNames.forEach((stepName, index) => { + code += generator.INDENT.repeat(2) + `case ${index}: # ${stepName}\n`; + let stepCode = generator.statementToCode(block, 'STEP_' + index); + if (stepCode !== '') { + code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); + } + let conditionCode = generator.valueToCode(block, 'CONDITION_' + index, Order.NONE) || 'False'; + code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; + code += generator.INDENT.repeat(4) + 'self.current_step_index += 1\n'; + }); + + generator.addClassMethodDefinition('steps', code); + + return '' } \ No newline at end of file From 6eb73b7d975e247dba15ba79ffc58eb421fe8b8e Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 25 Oct 2025 14:45:09 -0400 Subject: [PATCH 10/34] Limit Jump to step to be within steps --- src/blocks/mrc_jump_to_step.ts | 45 +++++++++++++++++++++-- src/blocks/mrc_steps.ts | 53 ++++++++++++++++++++++++++-- src/blocks/tokens.ts | 1 + src/i18n/locales/en/translation.json | 1 + src/i18n/locales/es/translation.json | 3 +- src/i18n/locales/he/translation.json | 5 +-- 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 2b377e68..e41a5949 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -25,14 +25,19 @@ import {Order} from 'blockly/python'; import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; import {MRC_STYLE_VARIABLES} from '../themes/styles'; +import {BLOCK_NAME as MRC_STEPS, StepsBlock} from './mrc_steps' export const BLOCK_NAME = 'mrc_jump_to_step'; const FIELD_STEP_NAME = 'STEP_NAME'; +const WARNING_ID_NOT_IN_STEP = 'not in step'; + + type JumpToStepBlock = Blockly.Block & Blockly.BlockSvg & JumpToStepMixin; interface JumpToStepMixin extends JumpToStepMixinType { + mrcHasWarning: boolean, } type JumpToStepMixinType = typeof JUMP_TO_STEP_BLOCK; @@ -50,6 +55,39 @@ const JUMP_TO_STEP_BLOCK = { this.setStyle(MRC_STYLE_VARIABLES); this.setTooltip('Jump to the specified step.'); }, + /** + * mrcOnMove is called when an EventBlock is moved. + */ + mrcOnMove: function(this: JumpToStepBlock, _reason: string[]): void { + this.checkBlockPlacement(); + }, + mrcOnAncestorMove: function(this: JumpToStepBlock): void { + this.checkBlockPlacement(); + }, + checkBlockPlacement: function(this: JumpToStepBlock): void { + const legalStepNames: string[] = []; + + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock.type === MRC_STEPS) { + // This block is within a class method definition. + const stepsBlock = rootBlock as StepsBlock; + // Add the method's parameter names to legalStepNames. + legalStepNames.push(...stepsBlock.mrcGetStepNames()); + } + + if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { + // If this blocks's parameter name is in legalParameterNames, it's good. + this.setWarningText(null, WARNING_ID_NOT_IN_STEP); + this.mrcHasWarning = false; + } else { + // Otherwise, add a warning to this block. + if (!this.mrcHasWarning) { + this.setWarningText(Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK, WARNING_ID_NOT_IN_STEP); + this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + this.mrcHasWarning = true; + } + } + }, }; export const setup = function() { @@ -60,8 +98,9 @@ export const pythonFromBlock = function( block: JumpToStepBlock, _generator: ExtendedPythonGenerator, ) { - // TODO (Alan) : Specify the type here as well - const code = '# TODO: Jump to step ' + block.getFieldValue(FIELD_STEP_NAME) + '\n'; + let code = 'self.current_step_index = self.mrc_step_name_to_index["' + + block.getFieldValue(FIELD_STEP_NAME) + '"]\n'; + code += 'return\n'; - return [code, Order.ATOMIC]; + return code; }; diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 3ead4aed..61d43197 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -24,13 +24,21 @@ import {Order} from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createStepFieldFlydown } from '../fields/field_flydown'; +import { createStepFieldFlydown, FieldFlydown } from '../fields/field_flydown'; import * as stepContainer from './mrc_step_container' export const BLOCK_NAME = 'mrc_steps'; // const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; +/** Extra state for serialising call_python_* blocks. */ +type StepsExtraState = { + /** + * The steps + */ + stepNames: string[], +}; + export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg; interface StepsMixin extends StepsMixinType { mrcStepNames: string[]; @@ -50,6 +58,17 @@ const STEPS = { this.setMutator(stepContainer.getMutatorIcon(this)); this.updateShape_(); }, + saveExtraState: function (this: StepsBlock): StepsExtraState{ + return { + stepNames: this.mrcStepNames, + }; + }, + loadExtraState: function (this: StepsBlock, state: StepsExtraState): void { + if (state && state.stepNames) { + this.mrcStepNames = state.stepNames; + this.updateShape_(); + } + }, compose: function (this: StepsBlock, containerBlock: Blockly.Block) { if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); @@ -80,6 +99,30 @@ const STEPS = { }, mrcOnChange: function(this: StepsBlock): void { + }, + mrcUpdateStepName: function(this: StepsBlock, step : number, newName: string) : string { + const otherNames = this.mrcStepNames.filter((_name, index) => index !== step); + let currentName = newName; + + // Make name unique if it conflicts + while (otherNames.includes(currentName)) { + // Check if currentName ends with a number + const match = currentName.match(/^(.*?)(\d+)$/); + if (match) { + // If it ends with a number, increment it + const baseName = match[1]; + const number = parseInt(match[2], 10); + currentName = baseName + (number + 1); + } else { + // If it doesn't end with a number, append 2 + currentName = currentName + '2'; + } + } + this.mrcStepNames[step] = currentName; + // TODO: Rename any jump blocks that refer to this step + + + return currentName; }, updateShape_: function (this: StepsBlock): void { // some way of knowing what was there before and what is there now @@ -91,13 +134,19 @@ const STEPS = { i++; } for (let j = 0; j < this.mrcStepNames.length; j++) { + const fieldFlydown = createStepFieldFlydown(this.mrcStepNames[j], true); + + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); this.appendValueInput('CONDITION_' + j) - .appendField(createStepFieldFlydown(this.mrcStepNames[j], true)) + .appendField(fieldFlydown) .setCheck('Boolean') .appendField(Blockly.Msg.REPEAT_UNTIL); this.appendStatementInput('STEP_' + j); } }, + mrcGetStepNames: function(this: StepsBlock): string[] { + return this.mrcStepNames; + } }; export const setup = function () { diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 8b817da0..0be5fc91 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -131,6 +131,7 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { COMMENT_DEFAULT_TEXT: t('BLOCKLY.COMMENT_DEFAULT_TEXT'), STEPS: t('BLOCKLY.STEPS'), REPEAT_UNTIL: t('BLOCKLY.REPEAT_UNTIL'), + JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK: t('BLOCKLY.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK'), } }; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 29fa0351..ea6b2af2 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -124,6 +124,7 @@ "PARAMETER": "parameter", "PARAMETERS": "Parameters", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", + "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "Jump can only go in their step's block", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "This event handler is already on the workspace.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "This block is an event handler for an event that no longer exists.", "EVENT_HANDLER_MECHANISM_EVENT_NOT_FOUND": "This block is an event handler for an event that no longer exists.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 897ae389..1dd98a78 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -125,7 +125,8 @@ "PARAMETER": "parámetro", "PARAMETERS": "Parámetros", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", - "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "Este controlador de eventos ya está en el área de trabajo.", + "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "El salto solo puede ir en el bloque de su paso", + "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "Este controlador de eventos ya está en el espacio de trabajo.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "Este bloque es un controlador de eventos para un evento que ya no existe.", "EVENT_HANDLER_MECHANISM_EVENT_NOT_FOUND": "Este bloque es un controlador de eventos para un evento que ya no existe.", "EVENT_HANDLER_MECHANISM_NOT_FOUND": "Este bloque es un controlador de eventos para un evento en un mecanismo que ya no existe.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 7b9d5bfc..202e61b9 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -123,8 +123,9 @@ "WHEN": "כאשר", "PARAMETER": "פרמטר", "PARAMETERS": "פרמטרים", - "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים להופיע רק בתוך הבלוק של המתודה שלהם.", - "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "מנהל האירועים הזה כבר נמצא בסביבת העבודה.", + "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים ללכת רק בבלוק השיטה שלהם", + "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "קפיצה יכולה ללכת רק בבלוק הצעד שלה", + "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "מטפל אירועים זה כבר נמצא במרחב העבודה.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "הבלוק הזה הוא מנהל אירועים לאירוע שכבר לא קיים.", "EVENT_HANDLER_MECHANISM_EVENT_NOT_FOUND": "הבלוק הזה הוא מנהל אירועים לאירוע שכבר לא קיים.", "EVENT_HANDLER_MECHANISM_NOT_FOUND": "הבלוק הזה הוא מנהל אירועים לאירוע במנגנון שכבר לא קיים.", From 12083624b7336c46b88c19070b7af1d1d715999a Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 25 Oct 2025 20:20:16 -0400 Subject: [PATCH 11/34] Move steps into opmode and only show when not already in opmode --- src/blocks/mrc_steps.ts | 5 +++++ src/editor/editor.ts | 8 ++++++++ src/toolbox/methods_category.ts | 7 +++++++ src/toolbox/test_category.ts | 5 ----- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 61d43197..2f7a0584 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -153,6 +153,11 @@ export const setup = function () { Blockly.Blocks[BLOCK_NAME] = STEPS; }; +export function isStepsInWorkspace(workspace: Blockly.Workspace): boolean { + const blocks = workspace.getBlocksByType(BLOCK_NAME); + return blocks.length > 0; +}; + export const pythonFromBlock = function ( block: StepsBlock, generator: ExtendedPythonGenerator, diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 50d7727c..66988bb1 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -29,6 +29,7 @@ import * as storageNames from '../storage/names'; import * as storageProject from '../storage/project'; import * as eventHandler from '../blocks/mrc_event_handler'; import * as classMethodDef from '../blocks/mrc_class_method_def'; +import * as blockSteps from '../blocks/mrc_steps'; import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_holder'; //import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests'; import { applyExpandedCategories, getToolboxJSON } from '../toolbox/toolbox'; @@ -378,6 +379,13 @@ export class Editor { this.blocklyWorkspace, methodNamesAlreadyOverridden); return methodNamesAlreadyOverridden; } + public isStepsInWorkspace(): boolean { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } + return blockSteps.isStepsInWorkspace(this.blocklyWorkspace); + } public getEventsFromWorkspace(): storageModuleContent.Event[] { if (!this.blocklyWorkspace.rendered) { diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 23695623..66a410b4 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -99,6 +99,13 @@ class MethodsCategory { methodNamesAlreadyOverridden, contents); break; case storageModule.ModuleType.OPMODE: + const hasSteps = editor.isStepsInWorkspace(); + if (!hasSteps) { + contents.push({ + kind: 'block', + type: 'mrc_steps', + }); + } // Add the methods for an OpMode. this.addClassBlocksForCurrentModule( Blockly.Msg['MORE_OPMODE_METHODS_LABEL'], this.opmodeClassBlocks, [], diff --git a/src/toolbox/test_category.ts b/src/toolbox/test_category.ts index 594416fb..f46179a3 100644 --- a/src/toolbox/test_category.ts +++ b/src/toolbox/test_category.ts @@ -20,11 +20,6 @@ export function getCategory(): toolboxItems.Category { addBuiltInFunctionBlocks([printFunction], contents); - contents.push({ - kind: 'block', - type: 'mrc_steps', - }); - return { kind: 'category', name: Blockly.Msg['MRC_CATEGORY_TEST'], From c4af155be952b06e606075ed9ff210f743c175ab Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 25 Oct 2025 20:22:45 -0400 Subject: [PATCH 12/34] Call steps as part of loop if it is defined --- server_python_scripts/blocks_base_classes/opmode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_python_scripts/blocks_base_classes/opmode.py b/server_python_scripts/blocks_base_classes/opmode.py index de597e53..7b48f8e6 100644 --- a/server_python_scripts/blocks_base_classes/opmode.py +++ b/server_python_scripts/blocks_base_classes/opmode.py @@ -7,6 +7,9 @@ def __init__(self, robot: RobotBase): def start(self) -> None: self.robot.start() def loop(self) -> None: + # Call steps method if it exists in the derived class + if hasattr(self, 'steps'): + self.steps() self.robot.update() def stop(self) -> None: self.robot.stop() From b84f6fd98818eb83557c829f4c5581a7b1754f9e Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 25 Oct 2025 20:24:17 -0400 Subject: [PATCH 13/34] Clean up warnings --- src/blocks/mrc_jump_to_step.ts | 1 - src/blocks/mrc_steps.ts | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index e41a5949..1b7883ec 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -20,7 +20,6 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import {Order} from 'blockly/python'; import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 2f7a0584..484174bf 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -24,7 +24,7 @@ import {Order} from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createStepFieldFlydown, FieldFlydown } from '../fields/field_flydown'; +import { createStepFieldFlydown } from '../fields/field_flydown'; import * as stepContainer from './mrc_step_container' export const BLOCK_NAME = 'mrc_steps'; @@ -75,8 +75,7 @@ const STEPS = { } const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - stepItemBlocks.forEach((stepItemBlock) => { - }); + this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { this.mrcStepNames.push(stepItemBlock.getName()); From 504948e5a01325f8dafd1c01a6b889b499fba8f4 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 25 Oct 2025 20:29:28 -0400 Subject: [PATCH 14/34] Ran format on new files --- src/blocks/mrc_jump_to_step.ts | 92 +++++++++++++++++----------------- src/blocks/mrc_steps.ts | 46 ++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 1b7883ec..bcc156a8 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -21,10 +21,10 @@ */ import * as Blockly from 'blockly'; -import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; -import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; -import {MRC_STYLE_VARIABLES} from '../themes/styles'; -import {BLOCK_NAME as MRC_STEPS, StepsBlock} from './mrc_steps' +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; +import { MRC_STYLE_VARIABLES } from '../themes/styles'; +import { BLOCK_NAME as MRC_STEPS, StepsBlock } from './mrc_steps' export const BLOCK_NAME = 'mrc_jump_to_step'; @@ -45,60 +45,60 @@ const JUMP_TO_STEP_BLOCK = { /** * Block initialization. */ - init: function(this: JumpToStepBlock): void { + init: function (this: JumpToStepBlock): void { this.appendDummyInput() - .appendField('Jump to') - .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); + .appendField('Jump to') + .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); this.setPreviousStatement(true, null); this.setInputsInline(true); this.setStyle(MRC_STYLE_VARIABLES); - this.setTooltip('Jump to the specified step.'); + this.setTooltip('Jump to the specified step.'); + }, + /** + * mrcOnMove is called when an EventBlock is moved. + */ + mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void { + this.checkBlockPlacement(); + }, + mrcOnAncestorMove: function (this: JumpToStepBlock): void { + this.checkBlockPlacement(); + }, + checkBlockPlacement: function (this: JumpToStepBlock): void { + const legalStepNames: string[] = []; + + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock.type === MRC_STEPS) { + // This block is within a class method definition. + const stepsBlock = rootBlock as StepsBlock; + // Add the method's parameter names to legalStepNames. + legalStepNames.push(...stepsBlock.mrcGetStepNames()); + } + + if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { + // If this blocks's parameter name is in legalParameterNames, it's good. + this.setWarningText(null, WARNING_ID_NOT_IN_STEP); + this.mrcHasWarning = false; + } else { + // Otherwise, add a warning to this block. + if (!this.mrcHasWarning) { + this.setWarningText(Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK, WARNING_ID_NOT_IN_STEP); + this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + this.mrcHasWarning = true; + } + } }, - /** - * mrcOnMove is called when an EventBlock is moved. - */ - mrcOnMove: function(this: JumpToStepBlock, _reason: string[]): void { - this.checkBlockPlacement(); - }, - mrcOnAncestorMove: function(this: JumpToStepBlock): void { - this.checkBlockPlacement(); - }, - checkBlockPlacement: function(this: JumpToStepBlock): void { - const legalStepNames: string[] = []; - - const rootBlock: Blockly.Block | null = this.getRootBlock(); - if (rootBlock.type === MRC_STEPS) { - // This block is within a class method definition. - const stepsBlock = rootBlock as StepsBlock; - // Add the method's parameter names to legalStepNames. - legalStepNames.push(...stepsBlock.mrcGetStepNames()); - } - - if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { - // If this blocks's parameter name is in legalParameterNames, it's good. - this.setWarningText(null, WARNING_ID_NOT_IN_STEP); - this.mrcHasWarning = false; - } else { - // Otherwise, add a warning to this block. - if (!this.mrcHasWarning) { - this.setWarningText(Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK, WARNING_ID_NOT_IN_STEP); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); - this.mrcHasWarning = true; - } - } - }, }; -export const setup = function() { +export const setup = function () { Blockly.Blocks[BLOCK_NAME] = JUMP_TO_STEP_BLOCK; }; -export const pythonFromBlock = function( - block: JumpToStepBlock, - _generator: ExtendedPythonGenerator, +export const pythonFromBlock = function ( + block: JumpToStepBlock, + _generator: ExtendedPythonGenerator, ) { let code = 'self.current_step_index = self.mrc_step_name_to_index["' + - block.getFieldValue(FIELD_STEP_NAME) + '"]\n'; + block.getFieldValue(FIELD_STEP_NAME) + '"]\n'; code += 'return\n'; return code; diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 484174bf..a6639cf0 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -20,7 +20,7 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import {Order} from 'blockly/python'; +import { Order } from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; @@ -33,10 +33,10 @@ export const BLOCK_NAME = 'mrc_steps'; /** Extra state for serialising call_python_* blocks. */ type StepsExtraState = { - /** - * The steps - */ - stepNames: string[], + /** + * The steps + */ + stepNames: string[], }; export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg; @@ -58,7 +58,7 @@ const STEPS = { this.setMutator(stepContainer.getMutatorIcon(this)); this.updateShape_(); }, - saveExtraState: function (this: StepsBlock): StepsExtraState{ + saveExtraState: function (this: StepsBlock): StepsExtraState { return { stepNames: this.mrcStepNames, }; @@ -75,7 +75,7 @@ const STEPS = { } const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - + this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { this.mrcStepNames.push(stepItemBlock.getName()); @@ -90,19 +90,19 @@ const STEPS = { }); return stepContainer.createMutatorBlocks(workspace, stepNames); }, - /** - * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. - */ - mrcOnMutatorOpen: function(this: StepsBlock): void { + /** + * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + */ + mrcOnMutatorOpen: function (this: StepsBlock): void { stepContainer.onMutatorOpen(this); }, - mrcOnChange: function(this: StepsBlock): void { - + mrcOnChange: function (this: StepsBlock): void { + }, - mrcUpdateStepName: function(this: StepsBlock, step : number, newName: string) : string { + mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { const otherNames = this.mrcStepNames.filter((_name, index) => index !== step); let currentName = newName; - + // Make name unique if it conflicts while (otherNames.includes(currentName)) { // Check if currentName ends with a number @@ -120,30 +120,30 @@ const STEPS = { this.mrcStepNames[step] = currentName; // TODO: Rename any jump blocks that refer to this step - + return currentName; }, updateShape_: function (this: StepsBlock): void { // some way of knowing what was there before and what is there now let success = true; let i = 0; - while (success){ + while (success) { success = this.removeInput('CONDITION_' + i, true); success = this.removeInput('STEP_' + i, true); i++; } for (let j = 0; j < this.mrcStepNames.length; j++) { const fieldFlydown = createStepFieldFlydown(this.mrcStepNames[j], true); - + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); this.appendValueInput('CONDITION_' + j) .appendField(fieldFlydown) .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); + .appendField(Blockly.Msg.REPEAT_UNTIL); this.appendStatementInput('STEP_' + j); } }, - mrcGetStepNames: function(this: StepsBlock): string[] { + mrcGetStepNames: function (this: StepsBlock): string[] { return this.mrcStepNames; } }; @@ -156,7 +156,7 @@ export function isStepsInWorkspace(workspace: Blockly.Workspace): boolean { const blocks = workspace.getBlocksByType(BLOCK_NAME); return blocks.length > 0; }; - + export const pythonFromBlock = function ( block: StepsBlock, generator: ExtendedPythonGenerator, @@ -169,7 +169,7 @@ export const pythonFromBlock = function ( code += generator.INDENT + `self.step_from_name['${stepName}'] = ${index}\n`; code += generator.INDENT + `self.name_from_step[${index}] = '${stepName}'\n`; }); - + code += generator.INDENT + 'self.current_step_index = 0\n'; code += generator.INDENT + 'self.initialized = True\n'; @@ -191,6 +191,6 @@ export const pythonFromBlock = function ( }); generator.addClassMethodDefinition('steps', code); - + return '' } \ No newline at end of file From fa32b2d9dbfaa3617db5bbf570326e5788b4495d Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sun, 26 Oct 2025 08:53:33 -0400 Subject: [PATCH 15/34] Simplify python generation --- src/blocks/mrc_steps.ts | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index a6639cf0..7f3c580c 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -161,33 +161,28 @@ export const pythonFromBlock = function ( block: StepsBlock, generator: ExtendedPythonGenerator, ) { + let code = 'def steps(self):\n'; + code += generator.INDENT + 'if not self._initialized_steps:\n'; + code += generator.INDENT.repeat(2) + 'self._current_step = "' + block.mrcStepNames[0] + '"\n'; + code += generator.INDENT.repeat(2) + 'self.initialized_steps = True\n\n'; + code += generator.INDENT + 'if self._current_step == None:\n'; + code += generator.INDENT.repeat(2) + 'return\n'; - let code = 'def initialize_steps(self):\n'; - code += generator.INDENT + 'self.step_from_name = {}\n'; - code += generator.INDENT + 'self.name_from_step = {}\n'; - block.mrcStepNames.forEach((stepName, index) => { - code += generator.INDENT + `self.step_from_name['${stepName}'] = ${index}\n`; - code += generator.INDENT + `self.name_from_step[${index}] = '${stepName}'\n`; - }); - - code += generator.INDENT + 'self.current_step_index = 0\n'; - code += generator.INDENT + 'self.initialized = True\n'; - generator.addClassMethodDefinition('initialize_steps', code); - - code = 'def steps(self):\n'; - code += generator.INDENT + 'if not self.initialized:\n'; - code += generator.INDENT.repeat(2) + 'self.initialize_steps()\n\n'; - code += generator.INDENT + 'match self.current_step_index:\n'; + code += generator.INDENT + 'match self._current_step:\n'; block.mrcStepNames.forEach((stepName, index) => { - code += generator.INDENT.repeat(2) + `case ${index}: # ${stepName}\n`; + code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; let stepCode = generator.statementToCode(block, 'STEP_' + index); if (stepCode !== '') { code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); } let conditionCode = generator.valueToCode(block, 'CONDITION_' + index, Order.NONE) || 'False'; code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; - code += generator.INDENT.repeat(4) + 'self.current_step_index += 1\n'; + if (index === block.mrcStepNames.length - 1) { + code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; + } else { + code += generator.INDENT.repeat(4) + 'self._current_step = "' + block.mrcStepNames[index + 1] + '"\n'; + } }); generator.addClassMethodDefinition('steps', code); From 1b5505555c75d69921734a4f9dcf18f0aa089f87 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sun, 26 Oct 2025 08:55:25 -0400 Subject: [PATCH 16/34] Simplify python generation --- src/blocks/mrc_jump_to_step.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index bcc156a8..83f42aa6 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -97,8 +97,8 @@ export const pythonFromBlock = function ( block: JumpToStepBlock, _generator: ExtendedPythonGenerator, ) { - let code = 'self.current_step_index = self.mrc_step_name_to_index["' + - block.getFieldValue(FIELD_STEP_NAME) + '"]\n'; + let code = 'self._current_step = "' + + block.getFieldValue(FIELD_STEP_NAME) + '"\n'; code += 'return\n'; return code; From e54d6965ab27660f801864cc2029a6866117391b Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sun, 26 Oct 2025 16:25:36 -0400 Subject: [PATCH 17/34] Updates jump correctly --- src/blocks/mrc_steps.ts | 117 ++++++++++++++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 16 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 7f3c580c..a59733c3 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -25,6 +25,7 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; +import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' export const BLOCK_NAME = 'mrc_steps'; @@ -76,11 +77,27 @@ const STEPS = { const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); + const oldStepNames = [...this.mrcStepNames]; this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { this.mrcStepNames.push(stepItemBlock.getName()); }); - // TODO: Update any jump blocks to have the correct name + + // Update jump blocks for any renamed steps + const workspace = this.workspace; + const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); + stepItemBlocks.forEach((stepItemBlock, index) => { + const oldName = stepItemBlock.getOriginalName(); + const newName = stepItemBlock.getName(); + if (oldName && oldName !== newName) { + jumpBlocks.forEach((jumpBlock) => { + if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { + jumpBlock.setFieldValue(newName, 'STEP_NAME'); + } + }); + } + }); + this.updateShape_(); }, decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { @@ -100,6 +117,7 @@ const STEPS = { }, mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { + const oldName = this.mrcStepNames[step]; const otherNames = this.mrcStepNames.filter((_name, index) => index !== step); let currentName = newName; @@ -118,29 +136,96 @@ const STEPS = { } } this.mrcStepNames[step] = currentName; - // TODO: Rename any jump blocks that refer to this step - + + // Update all mrc_jump_to_step blocks that reference the old name + if (oldName !== currentName) { + const workspace = this.workspace; + const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); + jumpBlocks.forEach((jumpBlock) => { + if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { + jumpBlock.setFieldValue(currentName, 'STEP_NAME'); + } + }); + } return currentName; }, updateShape_: function (this: StepsBlock): void { - // some way of knowing what was there before and what is there now - let success = true; + // Build a map of step names to their current input indices + const currentStepMap: { [stepName: string]: number } = {}; let i = 0; - while (success) { - success = this.removeInput('CONDITION_' + i, true); - success = this.removeInput('STEP_' + i, true); + while (this.getInput('CONDITION_' + i)) { + const conditionInput = this.getInput('CONDITION_' + i); + const field = conditionInput?.fieldRow[0]; + if (field) { + currentStepMap[field.getValue()] = i; + } i++; } + + // For each new step position, find where it currently is (if it exists) for (let j = 0; j < this.mrcStepNames.length; j++) { - const fieldFlydown = createStepFieldFlydown(this.mrcStepNames[j], true); - - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - this.appendValueInput('CONDITION_' + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput('STEP_' + j); + const stepName = this.mrcStepNames[j]; + const currentIndex = currentStepMap[stepName]; + + if (currentIndex !== undefined && currentIndex !== j) { + // Step exists but is at wrong position - move it + const conditionConnection = this.getInput('CONDITION_' + currentIndex)?.connection?.targetConnection; + const stepConnection = this.getInput('STEP_' + currentIndex)?.connection?.targetConnection; + + // Temporarily disconnect + if (conditionConnection) conditionConnection.disconnect(); + if (stepConnection) stepConnection.disconnect(); + + // Remove old inputs + this.removeInput('CONDITION_' + currentIndex, false); + this.removeInput('STEP_' + currentIndex, false); + + // Create new inputs at correct position + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); + + this.appendValueInput('CONDITION_' + j) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput('STEP_' + j); + + // Reconnect + if (conditionConnection) { + this.getInput('CONDITION_' + j)?.connection?.connect(conditionConnection); + } + if (stepConnection) { + this.getInput('STEP_' + j)?.connection?.connect(stepConnection); + } + + delete currentStepMap[stepName]; + } else if (currentIndex !== undefined) { + // Step is at correct position - just update the field + const conditionInput = this.getInput('CONDITION_' + j); + const field = conditionInput?.fieldRow[0]; + if (field && field.getValue() !== stepName) { + field.setValue(stepName); + } + delete currentStepMap[stepName]; + } else { + // Step doesn't exist - create it + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); + + this.appendValueInput('CONDITION_' + j) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput('STEP_' + j); + } + } + + // Remove any leftover inputs (steps that were deleted) + for (const stepName in currentStepMap) { + const index = currentStepMap[stepName]; + this.removeInput('CONDITION_' + index, false); + this.removeInput('STEP_' + index, false); } }, mrcGetStepNames: function (this: StepsBlock): string[] { From e18c2598b7fb5a6c2f19cb4ee3bf8a737d1ba3e1 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sun, 26 Oct 2025 16:27:35 -0400 Subject: [PATCH 18/34] remove warnings --- src/blocks/mrc_steps.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index a59733c3..956d8ea1 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -77,7 +77,6 @@ const STEPS = { const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - const oldStepNames = [...this.mrcStepNames]; this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { this.mrcStepNames.push(stepItemBlock.getName()); @@ -86,7 +85,7 @@ const STEPS = { // Update jump blocks for any renamed steps const workspace = this.workspace; const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - stepItemBlocks.forEach((stepItemBlock, index) => { + stepItemBlocks.forEach((stepItemBlock) => { const oldName = stepItemBlock.getOriginalName(); const newName = stepItemBlock.getName(); if (oldName && oldName !== newName) { From 5de62ad1762c23ddfa75abe2f4d6b62ff3b54ab2 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 27 Oct 2025 14:38:29 -0400 Subject: [PATCH 19/34] fix typo --- src/blocks/mrc_steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 956d8ea1..4a131b72 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -248,7 +248,7 @@ export const pythonFromBlock = function ( let code = 'def steps(self):\n'; code += generator.INDENT + 'if not self._initialized_steps:\n'; code += generator.INDENT.repeat(2) + 'self._current_step = "' + block.mrcStepNames[0] + '"\n'; - code += generator.INDENT.repeat(2) + 'self.initialized_steps = True\n\n'; + code += generator.INDENT.repeat(2) + 'self._initialized_steps = True\n\n'; code += generator.INDENT + 'if self._current_step == None:\n'; code += generator.INDENT.repeat(2) + 'return\n'; From acd2813c7750d1c8cf6a65152a0f6c1a640132ca Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:06:01 -0400 Subject: [PATCH 20/34] add Shadow true blocks --- src/blocks/mrc_steps.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 4a131b72..c3de2f7a 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -212,11 +212,21 @@ const STEPS = { const fieldFlydown = createStepFieldFlydown(stepName, true); fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - this.appendValueInput('CONDITION_' + j) + const conditionInput = this.appendValueInput('CONDITION_' + j) .appendField(fieldFlydown) .setCheck('Boolean') .appendField(Blockly.Msg.REPEAT_UNTIL); this.appendStatementInput('STEP_' + j); + + // Add shadow True block to the new condition input + if (this.workspace) { + const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; + shadowBlock.setShadow(true); + shadowBlock.setFieldValue('TRUE', 'BOOL'); + shadowBlock.initSvg(); + shadowBlock.render(); + conditionInput.connection?.connect(shadowBlock.outputConnection!); + } } } From 34571e70e3af9930d05bc1c711862e7b88643c25 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:20:25 -0400 Subject: [PATCH 21/34] Bump version to 0.0.4 --- src/storage/upgrade_project.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/storage/upgrade_project.ts b/src/storage/upgrade_project.ts index 1804d9c7..42a75117 100644 --- a/src/storage/upgrade_project.ts +++ b/src/storage/upgrade_project.ts @@ -31,7 +31,7 @@ import * as storageProject from './project'; import { ClassMethodDefBlock, BLOCK_NAME as MRC_CLASS_METHOD_DEF_BLOCK_NAME } from '../blocks/mrc_class_method_def'; export const NO_VERSION = '0.0.0'; -export const CURRENT_VERSION = '0.0.3'; +export const CURRENT_VERSION = '0.0.4'; export async function upgradeProjectIfNecessary( storage: commonStorage.Storage, projectName: string): Promise { @@ -45,8 +45,16 @@ export async function upgradeProjectIfNecessary( // @ts-ignore case '0.0.1': upgradeFrom_001_to_002(storage, projectName, projectInfo); + // Intentional fallthrough + // @ts-ignore case '0.0.2': upgradeFrom_002_to_003(storage, projectName, projectInfo); + // Intentional fallthrough + // @ts-ignore + case '0.0.3': + upgradeFrom_003_to_004(storage, projectName, projectInfo); + default: + throw new Error('Unrecognized project version: ' + projectInfo.version); } await storageProject.saveProjectInfo(storage, projectName); @@ -137,3 +145,12 @@ async function upgradeFrom_002_to_003( } projectInfo.version = '0.0.3'; } + +async function upgradeFrom_003_to_004( + _storage: commonStorage.Storage, + _projectName: string, + projectInfo: storageProject.ProjectInfo): Promise { + // The only change in this version are some new blocks. This keeps you + // from loading a project with an older version of software. + projectInfo.version = '0.0.4'; +} From f0e429869066f513f7a0b7681c7e841bb987fafe Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:21:53 -0400 Subject: [PATCH 22/34] Change case of Repeat Until --- src/i18n/locales/en/translation.json | 2 +- src/i18n/locales/es/translation.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index ea6b2af2..078e5875 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -148,7 +148,7 @@ "SET": "set", "TO": "to", "STEPS": "steps", - "REPEAT_UNTIL": "Repeat Until", + "REPEAT_UNTIL": "repeat until", "CUSTOM_EVENTS_LABEL": "Custom Events", "CUSTOM_METHODS_LABEL": "Custom Methods", "MORE_ROBOT_METHODS_LABEL": "More Robot Methods", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 1dd98a78..dba16e50 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -149,7 +149,7 @@ "SET": "establecer", "TO": "a", "STEPS": "pasos", - "REPEAT_UNTIL": "Repetir Hasta", + "REPEAT_UNTIL": "repetir hasta", "CUSTOM_EVENTS_LABEL": "Eventos Personalizados", "CUSTOM_METHODS_LABEL": "Métodos Personalizados", "MORE_ROBOT_METHODS_LABEL": "Más Métodos del Robot", From c888d11fdc79b7c576fd2f9d4ed44a47a70360d7 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:23:30 -0400 Subject: [PATCH 23/34] make sure self.steps is callable --- server_python_scripts/blocks_base_classes/opmode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_python_scripts/blocks_base_classes/opmode.py b/server_python_scripts/blocks_base_classes/opmode.py index 7b48f8e6..4ebd039d 100644 --- a/server_python_scripts/blocks_base_classes/opmode.py +++ b/server_python_scripts/blocks_base_classes/opmode.py @@ -8,7 +8,7 @@ def start(self) -> None: self.robot.start() def loop(self) -> None: # Call steps method if it exists in the derived class - if hasattr(self, 'steps'): + if hasattr(self, 'steps') and callable(self.steps): self.steps() self.robot.update() def stop(self) -> None: From a8775b9600abf77119c7aa6ab49aef39a6f74eb4 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:24:43 -0400 Subject: [PATCH 24/34] Change to 2 space indentation --- src/blocks/mrc_steps.ts | 436 ++++++++++++++++++++-------------------- 1 file changed, 218 insertions(+), 218 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index c3de2f7a..12ad90ae 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -34,252 +34,252 @@ export const BLOCK_NAME = 'mrc_steps'; /** Extra state for serialising call_python_* blocks. */ type StepsExtraState = { - /** - * The steps - */ - stepNames: string[], + /** + * The steps + */ + stepNames: string[], }; export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg; interface StepsMixin extends StepsMixinType { - mrcStepNames: string[]; + mrcStepNames: string[]; } type StepsMixinType = typeof STEPS; const STEPS = { - /** - * Block initialization. - */ - init: function (this: StepsBlock): void { - this.mrcStepNames = ["0"]; - this.appendDummyInput() - .appendField(Blockly.Msg.STEPS); - this.setInputsInline(false); - this.setStyle(MRC_STYLE_STEPS); - this.setMutator(stepContainer.getMutatorIcon(this)); - this.updateShape_(); - }, - saveExtraState: function (this: StepsBlock): StepsExtraState { - return { - stepNames: this.mrcStepNames, - }; - }, - loadExtraState: function (this: StepsBlock, state: StepsExtraState): void { - if (state && state.stepNames) { - this.mrcStepNames = state.stepNames; - this.updateShape_(); - } - }, - compose: function (this: StepsBlock, containerBlock: Blockly.Block) { - if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { - throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); - } - const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; - const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); + /** + * Block initialization. + */ + init: function (this: StepsBlock): void { + this.mrcStepNames = ["0"]; + this.appendDummyInput() + .appendField(Blockly.Msg.STEPS); + this.setInputsInline(false); + this.setStyle(MRC_STYLE_STEPS); + this.setMutator(stepContainer.getMutatorIcon(this)); + this.updateShape_(); + }, + saveExtraState: function (this: StepsBlock): StepsExtraState { + return { + stepNames: this.mrcStepNames, + }; + }, + loadExtraState: function (this: StepsBlock, state: StepsExtraState): void { + if (state && state.stepNames) { + this.mrcStepNames = state.stepNames; + this.updateShape_(); + } + }, + compose: function (this: StepsBlock, containerBlock: Blockly.Block) { + if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { + throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); + } + const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; + const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - this.mrcStepNames = []; - stepItemBlocks.forEach((stepItemBlock) => { - this.mrcStepNames.push(stepItemBlock.getName()); - }); - - // Update jump blocks for any renamed steps - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - stepItemBlocks.forEach((stepItemBlock) => { - const oldName = stepItemBlock.getOriginalName(); - const newName = stepItemBlock.getName(); - if (oldName && oldName !== newName) { - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(newName, 'STEP_NAME'); - } - }); - } - }); - - this.updateShape_(); - }, - decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { - const stepNames: string[] = []; - this.mrcStepNames.forEach(step => { - stepNames.push(step); + this.mrcStepNames = []; + stepItemBlocks.forEach((stepItemBlock) => { + this.mrcStepNames.push(stepItemBlock.getName()); + }); + + // Update jump blocks for any renamed steps + const workspace = this.workspace; + const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); + stepItemBlocks.forEach((stepItemBlock) => { + const oldName = stepItemBlock.getOriginalName(); + const newName = stepItemBlock.getName(); + if (oldName && oldName !== newName) { + jumpBlocks.forEach((jumpBlock) => { + if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { + jumpBlock.setFieldValue(newName, 'STEP_NAME'); + } }); - return stepContainer.createMutatorBlocks(workspace, stepNames); - }, - /** - * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. - */ - mrcOnMutatorOpen: function (this: StepsBlock): void { - stepContainer.onMutatorOpen(this); - }, - mrcOnChange: function (this: StepsBlock): void { - - }, - mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { - const oldName = this.mrcStepNames[step]; - const otherNames = this.mrcStepNames.filter((_name, index) => index !== step); - let currentName = newName; - - // Make name unique if it conflicts - while (otherNames.includes(currentName)) { - // Check if currentName ends with a number - const match = currentName.match(/^(.*?)(\d+)$/); - if (match) { - // If it ends with a number, increment it - const baseName = match[1]; - const number = parseInt(match[2], 10); - currentName = baseName + (number + 1); - } else { - // If it doesn't end with a number, append 2 - currentName = currentName + '2'; - } - } - this.mrcStepNames[step] = currentName; - - // Update all mrc_jump_to_step blocks that reference the old name - if (oldName !== currentName) { - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(currentName, 'STEP_NAME'); - } - }); + } + }); + + this.updateShape_(); + }, + decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { + const stepNames: string[] = []; + this.mrcStepNames.forEach(step => { + stepNames.push(step); + }); + return stepContainer.createMutatorBlocks(workspace, stepNames); + }, + /** + * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + */ + mrcOnMutatorOpen: function (this: StepsBlock): void { + stepContainer.onMutatorOpen(this); + }, + mrcOnChange: function (this: StepsBlock): void { + + }, + mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { + const oldName = this.mrcStepNames[step]; + const otherNames = this.mrcStepNames.filter((_name, index) => index !== step); + let currentName = newName; + + // Make name unique if it conflicts + while (otherNames.includes(currentName)) { + // Check if currentName ends with a number + const match = currentName.match(/^(.*?)(\d+)$/); + if (match) { + // If it ends with a number, increment it + const baseName = match[1]; + const number = parseInt(match[2], 10); + currentName = baseName + (number + 1); + } else { + // If it doesn't end with a number, append 2 + currentName = currentName + '2'; + } + } + this.mrcStepNames[step] = currentName; + + // Update all mrc_jump_to_step blocks that reference the old name + if (oldName !== currentName) { + const workspace = this.workspace; + const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); + jumpBlocks.forEach((jumpBlock) => { + if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { + jumpBlock.setFieldValue(currentName, 'STEP_NAME'); } + }); + } + + return currentName; + }, + updateShape_: function (this: StepsBlock): void { + // Build a map of step names to their current input indices + const currentStepMap: { [stepName: string]: number } = {}; + let i = 0; + while (this.getInput('CONDITION_' + i)) { + const conditionInput = this.getInput('CONDITION_' + i); + const field = conditionInput?.fieldRow[0]; + if (field) { + currentStepMap[field.getValue()] = i; + } + i++; + } + + // For each new step position, find where it currently is (if it exists) + for (let j = 0; j < this.mrcStepNames.length; j++) { + const stepName = this.mrcStepNames[j]; + const currentIndex = currentStepMap[stepName]; + + if (currentIndex !== undefined && currentIndex !== j) { + // Step exists but is at wrong position - move it + const conditionConnection = this.getInput('CONDITION_' + currentIndex)?.connection?.targetConnection; + const stepConnection = this.getInput('STEP_' + currentIndex)?.connection?.targetConnection; + + // Temporarily disconnect + if (conditionConnection) conditionConnection.disconnect(); + if (stepConnection) stepConnection.disconnect(); + + // Remove old inputs + this.removeInput('CONDITION_' + currentIndex, false); + this.removeInput('STEP_' + currentIndex, false); - return currentName; - }, - updateShape_: function (this: StepsBlock): void { - // Build a map of step names to their current input indices - const currentStepMap: { [stepName: string]: number } = {}; - let i = 0; - while (this.getInput('CONDITION_' + i)) { - const conditionInput = this.getInput('CONDITION_' + i); - const field = conditionInput?.fieldRow[0]; - if (field) { - currentStepMap[field.getValue()] = i; - } - i++; + // Create new inputs at correct position + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); + + this.appendValueInput('CONDITION_' + j) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput('STEP_' + j); + + // Reconnect + if (conditionConnection) { + this.getInput('CONDITION_' + j)?.connection?.connect(conditionConnection); + } + if (stepConnection) { + this.getInput('STEP_' + j)?.connection?.connect(stepConnection); } - // For each new step position, find where it currently is (if it exists) - for (let j = 0; j < this.mrcStepNames.length; j++) { - const stepName = this.mrcStepNames[j]; - const currentIndex = currentStepMap[stepName]; - - if (currentIndex !== undefined && currentIndex !== j) { - // Step exists but is at wrong position - move it - const conditionConnection = this.getInput('CONDITION_' + currentIndex)?.connection?.targetConnection; - const stepConnection = this.getInput('STEP_' + currentIndex)?.connection?.targetConnection; - - // Temporarily disconnect - if (conditionConnection) conditionConnection.disconnect(); - if (stepConnection) stepConnection.disconnect(); - - // Remove old inputs - this.removeInput('CONDITION_' + currentIndex, false); - this.removeInput('STEP_' + currentIndex, false); - - // Create new inputs at correct position - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - this.appendValueInput('CONDITION_' + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput('STEP_' + j); - - // Reconnect - if (conditionConnection) { - this.getInput('CONDITION_' + j)?.connection?.connect(conditionConnection); - } - if (stepConnection) { - this.getInput('STEP_' + j)?.connection?.connect(stepConnection); - } - - delete currentStepMap[stepName]; - } else if (currentIndex !== undefined) { - // Step is at correct position - just update the field - const conditionInput = this.getInput('CONDITION_' + j); - const field = conditionInput?.fieldRow[0]; - if (field && field.getValue() !== stepName) { - field.setValue(stepName); - } - delete currentStepMap[stepName]; - } else { - // Step doesn't exist - create it - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - const conditionInput = this.appendValueInput('CONDITION_' + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput('STEP_' + j); - - // Add shadow True block to the new condition input - if (this.workspace) { - const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; - shadowBlock.setShadow(true); - shadowBlock.setFieldValue('TRUE', 'BOOL'); - shadowBlock.initSvg(); - shadowBlock.render(); - conditionInput.connection?.connect(shadowBlock.outputConnection!); - } - } + delete currentStepMap[stepName]; + } else if (currentIndex !== undefined) { + // Step is at correct position - just update the field + const conditionInput = this.getInput('CONDITION_' + j); + const field = conditionInput?.fieldRow[0]; + if (field && field.getValue() !== stepName) { + field.setValue(stepName); } + delete currentStepMap[stepName]; + } else { + // Step doesn't exist - create it + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); + + const conditionInput = this.appendValueInput('CONDITION_' + j) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput('STEP_' + j); - // Remove any leftover inputs (steps that were deleted) - for (const stepName in currentStepMap) { - const index = currentStepMap[stepName]; - this.removeInput('CONDITION_' + index, false); - this.removeInput('STEP_' + index, false); + // Add shadow True block to the new condition input + if (this.workspace) { + const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; + shadowBlock.setShadow(true); + shadowBlock.setFieldValue('TRUE', 'BOOL'); + shadowBlock.initSvg(); + shadowBlock.render(); + conditionInput.connection?.connect(shadowBlock.outputConnection!); } - }, - mrcGetStepNames: function (this: StepsBlock): string[] { - return this.mrcStepNames; + } } + + // Remove any leftover inputs (steps that were deleted) + for (const stepName in currentStepMap) { + const index = currentStepMap[stepName]; + this.removeInput('CONDITION_' + index, false); + this.removeInput('STEP_' + index, false); + } + }, + mrcGetStepNames: function (this: StepsBlock): string[] { + return this.mrcStepNames; + } }; export const setup = function () { - Blockly.Blocks[BLOCK_NAME] = STEPS; + Blockly.Blocks[BLOCK_NAME] = STEPS; }; export function isStepsInWorkspace(workspace: Blockly.Workspace): boolean { - const blocks = workspace.getBlocksByType(BLOCK_NAME); - return blocks.length > 0; + const blocks = workspace.getBlocksByType(BLOCK_NAME); + return blocks.length > 0; }; export const pythonFromBlock = function ( - block: StepsBlock, - generator: ExtendedPythonGenerator, + block: StepsBlock, + generator: ExtendedPythonGenerator, ) { - let code = 'def steps(self):\n'; - code += generator.INDENT + 'if not self._initialized_steps:\n'; - code += generator.INDENT.repeat(2) + 'self._current_step = "' + block.mrcStepNames[0] + '"\n'; - code += generator.INDENT.repeat(2) + 'self._initialized_steps = True\n\n'; - code += generator.INDENT + 'if self._current_step == None:\n'; - code += generator.INDENT.repeat(2) + 'return\n'; - - - code += generator.INDENT + 'match self._current_step:\n'; - block.mrcStepNames.forEach((stepName, index) => { - code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; - let stepCode = generator.statementToCode(block, 'STEP_' + index); - if (stepCode !== '') { - code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); - } - let conditionCode = generator.valueToCode(block, 'CONDITION_' + index, Order.NONE) || 'False'; - code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; - if (index === block.mrcStepNames.length - 1) { - code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; - } else { - code += generator.INDENT.repeat(4) + 'self._current_step = "' + block.mrcStepNames[index + 1] + '"\n'; - } - }); + let code = 'def steps(self):\n'; + code += generator.INDENT + 'if not self._initialized_steps:\n'; + code += generator.INDENT.repeat(2) + 'self._current_step = "' + block.mrcStepNames[0] + '"\n'; + code += generator.INDENT.repeat(2) + 'self._initialized_steps = True\n\n'; + code += generator.INDENT + 'if self._current_step == None:\n'; + code += generator.INDENT.repeat(2) + 'return\n'; + + + code += generator.INDENT + 'match self._current_step:\n'; + block.mrcStepNames.forEach((stepName, index) => { + code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; + let stepCode = generator.statementToCode(block, 'STEP_' + index); + if (stepCode !== '') { + code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); + } + let conditionCode = generator.valueToCode(block, 'CONDITION_' + index, Order.NONE) || 'False'; + code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; + if (index === block.mrcStepNames.length - 1) { + code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; + } else { + code += generator.INDENT.repeat(4) + 'self._current_step = "' + block.mrcStepNames[index + 1] + '"\n'; + } + }); - generator.addClassMethodDefinition('steps', code); + generator.addClassMethodDefinition('steps', code); - return '' + return '' } \ No newline at end of file From 2c19451a10f829f45d4dc203b7f49267f7999db4 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 29 Oct 2025 21:25:19 -0400 Subject: [PATCH 25/34] change to 2 space indentation --- src/blocks/mrc_step_container.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blocks/mrc_step_container.ts b/src/blocks/mrc_step_container.ts index 1e11ed64..c2a96ccf 100644 --- a/src/blocks/mrc_step_container.ts +++ b/src/blocks/mrc_step_container.ts @@ -35,7 +35,7 @@ export const setup = function () { const INPUT_STACK = 'STACK'; export type StepContainerBlock = StepContainerMixin & Blockly.BlockSvg; -interface StepContainerMixin extends StepContainerMixinType {} +interface StepContainerMixin extends StepContainerMixinType { } type StepContainerMixinType = typeof STEP_CONTAINER; const STEP_CONTAINER = { @@ -50,7 +50,7 @@ const STEP_CONTAINER = { let block = this.getInputTargetBlock(INPUT_STACK); while (block && !block.isInsertionMarker()) { if (block.type !== STEP_ITEM_BLOCK_NAME) { - throw new Error('getItemNames: block.type should be ' + STEP_ITEM_BLOCK_NAME); + throw new Error('getItemNames: block.type should be ' + STEP_ITEM_BLOCK_NAME); } stepItemBlocks.push(block as StepItemBlock); block = block.nextConnection && block.nextConnection.targetBlock(); @@ -73,7 +73,7 @@ type StepItemMixinType = typeof STEP_ITEM; const STEP_ITEM = { init: function (this: StepItemBlock) { this.appendDummyInput() - .appendField(new Blockly.FieldTextInput(''), FIELD_NAME); + .appendField(new Blockly.FieldTextInput(''), FIELD_NAME); this.setPreviousStatement(true); this.setNextStatement(true); this.setStyle(MRC_STYLE_CLASS_BLOCKS); @@ -129,7 +129,7 @@ function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { workspace.getBlocksByType(STEP_ITEM_BLOCK_NAME, false).forEach(block => { usedNames.push(block.getFieldValue(FIELD_NAME)); }); - + // Find the first unused number starting from 0 let counter = 0; let uniqueName = counter.toString(); @@ -137,7 +137,7 @@ function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { counter++; uniqueName = counter.toString(); } - + const jsonBlock = { kind: 'block', type: STEP_ITEM_BLOCK_NAME, From e8346ca2262ed904b27666e28789296907514750 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 30 Oct 2025 19:22:50 -0400 Subject: [PATCH 26/34] Addressed review comments --- src/blocks/mrc_class_method_def.ts | 2 +- src/blocks/mrc_steps.ts | 10 +++++----- src/storage/upgrade_project.ts | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index ad0e220c..d6b8126f 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -62,7 +62,7 @@ interface ClassMethodDefMixin extends ClassMethodDefMixinType { } type ClassMethodDefMixinType = typeof CLASS_METHOD_DEF; -/** Extra state for serialising call_python_* blocks. */ +/** Extra state for serialising mrc_class_method_def blocks. */ type ClassMethodDefExtraState = { /** * The id that identifies this method definition. diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 12ad90ae..0c8448ba 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -29,10 +29,8 @@ import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' export const BLOCK_NAME = 'mrc_steps'; -// const MUTATOR_BLOCK_NAME = 'steps_mutatorarg'; - -/** Extra state for serialising call_python_* blocks. */ +/** Extra state for serialising mrc_steps blocks. */ type StepsExtraState = { /** * The steps @@ -223,8 +221,10 @@ const STEPS = { const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; shadowBlock.setShadow(true); shadowBlock.setFieldValue('TRUE', 'BOOL'); - shadowBlock.initSvg(); - shadowBlock.render(); + if (this.workspace.rendered){ + shadowBlock.initSvg(); + shadowBlock.render(); + } conditionInput.connection?.connect(shadowBlock.outputConnection!); } } diff --git a/src/storage/upgrade_project.ts b/src/storage/upgrade_project.ts index 42a75117..78c39f07 100644 --- a/src/storage/upgrade_project.ts +++ b/src/storage/upgrade_project.ts @@ -48,11 +48,10 @@ export async function upgradeProjectIfNecessary( // Intentional fallthrough // @ts-ignore case '0.0.2': - upgradeFrom_002_to_003(storage, projectName, projectInfo); - // Intentional fallthrough - // @ts-ignore + upgradeFrom_002_to_003(storage, projectName, projectInfo); case '0.0.3': upgradeFrom_003_to_004(storage, projectName, projectInfo); + break; default: throw new Error('Unrecognized project version: ' + projectInfo.version); From b08d01f0064c8510c40d25648f6f30285812365b Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Thu, 30 Oct 2025 23:30:56 -0700 Subject: [PATCH 27/34] In mrc_steps.ts: Initialize mrcStepNames to []. Removed "if (state && state.stepNames) {" from loadExtraState. Add braces to if statements. Changed python code that checks whether _initialize_steps has been set. Added createStepsBlocks function to create the steps block for the toolbox. In methods_category.ts: Modified MethodsCategory.methodsFlyout to call createStepsBlocks to create the mrc_steps block for the toolbox. --- src/blocks/mrc_steps.ts | 32 +++++++++++++++++++++++--------- src/toolbox/methods_category.ts | 6 ++---- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 0c8448ba..5573bf26 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -27,6 +27,8 @@ import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' +import * as value from './utils/value'; +import * as toolboxItems from '../toolbox/items'; export const BLOCK_NAME = 'mrc_steps'; @@ -49,7 +51,7 @@ const STEPS = { * Block initialization. */ init: function (this: StepsBlock): void { - this.mrcStepNames = ["0"]; + this.mrcStepNames = []; this.appendDummyInput() .appendField(Blockly.Msg.STEPS); this.setInputsInline(false); @@ -63,10 +65,8 @@ const STEPS = { }; }, loadExtraState: function (this: StepsBlock, state: StepsExtraState): void { - if (state && state.stepNames) { - this.mrcStepNames = state.stepNames; - this.updateShape_(); - } + this.mrcStepNames = state.stepNames; + this.updateShape_(); }, compose: function (this: StepsBlock, containerBlock: Blockly.Block) { if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { @@ -171,8 +171,12 @@ const STEPS = { const stepConnection = this.getInput('STEP_' + currentIndex)?.connection?.targetConnection; // Temporarily disconnect - if (conditionConnection) conditionConnection.disconnect(); - if (stepConnection) stepConnection.disconnect(); + if (conditionConnection) { + conditionConnection.disconnect(); + } + if (stepConnection) { + stepConnection.disconnect(); + } // Remove old inputs this.removeInput('CONDITION_' + currentIndex, false); @@ -256,7 +260,7 @@ export const pythonFromBlock = function ( generator: ExtendedPythonGenerator, ) { let code = 'def steps(self):\n'; - code += generator.INDENT + 'if not self._initialized_steps:\n'; + code += generator.INDENT + 'if not hasattr(self, "_initialized_steps"):\n'; code += generator.INDENT.repeat(2) + 'self._current_step = "' + block.mrcStepNames[0] + '"\n'; code += generator.INDENT.repeat(2) + 'self._initialized_steps = True\n\n'; code += generator.INDENT + 'if self._current_step == None:\n'; @@ -282,4 +286,14 @@ export const pythonFromBlock = function ( generator.addClassMethodDefinition('steps', code); return '' -} \ No newline at end of file +} + +export function createStepsBlock(): toolboxItems.Block { + const extraState: StepsExtraState = { + stepNames: ['0'], + }; + const fields: {[key: string]: any} = {}; + const inputs: {[key: string]: any} = {}; + inputs['CONDITION_' + 0] = value.createBooleanShadowValue(true); + return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); +} diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 66a410b4..163d259a 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -27,6 +27,7 @@ import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles'; import { CLASS_NAME_ROBOT_BASE, CLASS_NAME_OPMODE, CLASS_NAME_MECHANISM } from '../blocks/utils/python'; import { addInstanceWithinBlocks } from '../blocks/mrc_call_python_function'; import { createCustomMethodBlock, getBaseClassBlocks, FIELD_METHOD_NAME, createCustomMethodBlockWithReturn } from '../blocks/mrc_class_method_def'; +import { createStepsBlock } from '../blocks/mrc_steps'; import { Editor } from '../editor/editor'; @@ -101,10 +102,7 @@ class MethodsCategory { case storageModule.ModuleType.OPMODE: const hasSteps = editor.isStepsInWorkspace(); if (!hasSteps) { - contents.push({ - kind: 'block', - type: 'mrc_steps', - }); + contents.push(createStepsBlock()); } // Add the methods for an OpMode. this.addClassBlocksForCurrentModule( From 06109957c07ad8f5c4f10a65d227fcfdbb814b0d Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Thu, 30 Oct 2025 23:36:50 -0700 Subject: [PATCH 28/34] Added constants for INPUT_CONDITION_PREFIX and INPUT_STEP_PREFIX. --- src/blocks/mrc_steps.ts | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 5573bf26..10b28eb7 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -32,6 +32,9 @@ import * as toolboxItems from '../toolbox/items'; export const BLOCK_NAME = 'mrc_steps'; +const INPUT_CONDITION_PREFIX = 'CONDITION_'; +const INPUT_STEP_PREFIX = 'STEP_'; + /** Extra state for serialising mrc_steps blocks. */ type StepsExtraState = { /** @@ -151,8 +154,8 @@ const STEPS = { // Build a map of step names to their current input indices const currentStepMap: { [stepName: string]: number } = {}; let i = 0; - while (this.getInput('CONDITION_' + i)) { - const conditionInput = this.getInput('CONDITION_' + i); + while (this.getInput(INPUT_CONDITION_PREFIX + i)) { + const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); const field = conditionInput?.fieldRow[0]; if (field) { currentStepMap[field.getValue()] = i; @@ -167,8 +170,8 @@ const STEPS = { if (currentIndex !== undefined && currentIndex !== j) { // Step exists but is at wrong position - move it - const conditionConnection = this.getInput('CONDITION_' + currentIndex)?.connection?.targetConnection; - const stepConnection = this.getInput('STEP_' + currentIndex)?.connection?.targetConnection; + const conditionConnection = this.getInput(INPUT_CONDITION_PREFIX + currentIndex)?.connection?.targetConnection; + const stepConnection = this.getInput(INPUT_STEP_PREFIX + currentIndex)?.connection?.targetConnection; // Temporarily disconnect if (conditionConnection) { @@ -179,31 +182,31 @@ const STEPS = { } // Remove old inputs - this.removeInput('CONDITION_' + currentIndex, false); - this.removeInput('STEP_' + currentIndex, false); + this.removeInput(INPUT_CONDITION_PREFIX + currentIndex, false); + this.removeInput(INPUT_STEP_PREFIX + currentIndex, false); // Create new inputs at correct position const fieldFlydown = createStepFieldFlydown(stepName, true); fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - this.appendValueInput('CONDITION_' + j) + this.appendValueInput(INPUT_CONDITION_PREFIX + j) .appendField(fieldFlydown) .setCheck('Boolean') .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput('STEP_' + j); + this.appendStatementInput(INPUT_STEP_PREFIX + j); // Reconnect if (conditionConnection) { - this.getInput('CONDITION_' + j)?.connection?.connect(conditionConnection); + this.getInput(INPUT_CONDITION_PREFIX + j)?.connection?.connect(conditionConnection); } if (stepConnection) { - this.getInput('STEP_' + j)?.connection?.connect(stepConnection); + this.getInput(INPUT_STEP_PREFIX + j)?.connection?.connect(stepConnection); } delete currentStepMap[stepName]; } else if (currentIndex !== undefined) { // Step is at correct position - just update the field - const conditionInput = this.getInput('CONDITION_' + j); + const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + j); const field = conditionInput?.fieldRow[0]; if (field && field.getValue() !== stepName) { field.setValue(stepName); @@ -214,11 +217,11 @@ const STEPS = { const fieldFlydown = createStepFieldFlydown(stepName, true); fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - const conditionInput = this.appendValueInput('CONDITION_' + j) + const conditionInput = this.appendValueInput(INPUT_CONDITION_PREFIX + j) .appendField(fieldFlydown) .setCheck('Boolean') .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput('STEP_' + j); + this.appendStatementInput(INPUT_STEP_PREFIX + j); // Add shadow True block to the new condition input if (this.workspace) { @@ -237,8 +240,8 @@ const STEPS = { // Remove any leftover inputs (steps that were deleted) for (const stepName in currentStepMap) { const index = currentStepMap[stepName]; - this.removeInput('CONDITION_' + index, false); - this.removeInput('STEP_' + index, false); + this.removeInput(INPUT_CONDITION_PREFIX + index, false); + this.removeInput(INPUT_STEP_PREFIX + index, false); } }, mrcGetStepNames: function (this: StepsBlock): string[] { @@ -270,11 +273,11 @@ export const pythonFromBlock = function ( code += generator.INDENT + 'match self._current_step:\n'; block.mrcStepNames.forEach((stepName, index) => { code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; - let stepCode = generator.statementToCode(block, 'STEP_' + index); + let stepCode = generator.statementToCode(block, INPUT_STEP_PREFIX + index); if (stepCode !== '') { code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); } - let conditionCode = generator.valueToCode(block, 'CONDITION_' + index, Order.NONE) || 'False'; + let conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False'; code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; if (index === block.mrcStepNames.length - 1) { code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; @@ -294,6 +297,6 @@ export function createStepsBlock(): toolboxItems.Block { }; const fields: {[key: string]: any} = {}; const inputs: {[key: string]: any} = {}; - inputs['CONDITION_' + 0] = value.createBooleanShadowValue(true); + inputs[INPUT_CONDITION_PREFIX + 0] = value.createBooleanShadowValue(true); return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); } From dfd03a48474dd96bc5f9459ce303875c6e3fafe5 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Thu, 30 Oct 2025 23:38:50 -0700 Subject: [PATCH 29/34] Moved code to add shadow True blocks from updateShape_ to compose. --- src/blocks/mrc_steps.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 10b28eb7..71aa3cc7 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -99,6 +99,21 @@ const STEPS = { }); this.updateShape_(); + + // Add a shadow True block to each empty condition input. + for (var i = 0; i < this.mrcStepNames.length; i++) { + const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); + if (conditionInput && !conditionInput.connection?.targetConnection) { + const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; + shadowBlock.setShadow(true); + shadowBlock.setFieldValue('TRUE', 'BOOL'); + if (this.workspace.rendered) { + shadowBlock.initSvg(); + shadowBlock.render(); + } + conditionInput.connection?.connect(shadowBlock.outputConnection!); + } + } }, decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { const stepNames: string[] = []; @@ -217,23 +232,11 @@ const STEPS = { const fieldFlydown = createStepFieldFlydown(stepName, true); fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - const conditionInput = this.appendValueInput(INPUT_CONDITION_PREFIX + j) + this.appendValueInput(INPUT_CONDITION_PREFIX + j) .appendField(fieldFlydown) .setCheck('Boolean') .appendField(Blockly.Msg.REPEAT_UNTIL); this.appendStatementInput(INPUT_STEP_PREFIX + j); - - // Add shadow True block to the new condition input - if (this.workspace) { - const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; - shadowBlock.setShadow(true); - shadowBlock.setFieldValue('TRUE', 'BOOL'); - if (this.workspace.rendered){ - shadowBlock.initSvg(); - shadowBlock.render(); - } - conditionInput.connection?.connect(shadowBlock.outputConnection!); - } } } From f8f4f352518b7dc39bb46f2b32dff6e25b0e475a Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 3 Nov 2025 21:48:27 -0800 Subject: [PATCH 30/34] In mrc_jump_to_steps: Added renameSteps function. In mrc_step_container: Added conditionShadowState, conditionTargetConnection, statementTargetConnection to StepItemMixin. In mrc_steps: Renamed INPUT_STEP_PREFIX to INPUT_STATEMENT_PREFIX. Added saveConnections method to keep track of connections during mutation. (This is what blockly's controls_if block does.) Changed compose method to reconnect connections. (This is what blockly's controls_if block does.) Call renameSteps (from mrc_jump_to_step) when steps are renamed. Changed updateShape_ to remove all inputs and create new ones. (This is what blockly's controls_if block does.) --- src/blocks/mrc_jump_to_step.ts | 10 ++ src/blocks/mrc_step_container.ts | 9 +- src/blocks/mrc_steps.ts | 230 +++++++++++++------------------ 3 files changed, 108 insertions(+), 141 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 83f42aa6..50f940ff 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -103,3 +103,13 @@ export const pythonFromBlock = function ( return code; }; + +export function renameSteps(workspace: Blockly.Workspace, mapOldStepNameToNewStepName: {[newStepName: string]: string}): void { + workspace.getBlocksByType(BLOCK_NAME, false).forEach((jumpBlock) => { + const stepName = jumpBlock.getFieldValue(FIELD_STEP_NAME); + if (stepName in mapOldStepNameToNewStepName) { + const newStepName = mapOldStepNameToNewStepName[stepName]; + jumpBlock.setFieldValue(newStepName, FIELD_STEP_NAME); + } + }); +} diff --git a/src/blocks/mrc_step_container.ts b/src/blocks/mrc_step_container.ts index c2a96ccf..c8d74f0b 100644 --- a/src/blocks/mrc_step_container.ts +++ b/src/blocks/mrc_step_container.ts @@ -66,6 +66,9 @@ const FIELD_NAME = 'NAME'; export type StepItemBlock = StepItemMixin & Blockly.BlockSvg; interface StepItemMixin extends StepItemMixinType { originalName: string, + conditionShadowState?: any; + conditionTargetConnection?: Blockly.Connection | null; + statementTargetConnection?: Blockly.Connection | null; } type StepItemMixinType = typeof STEP_ITEM; @@ -174,7 +177,7 @@ function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abs } /** - * Called for mrc_event and mrc_class_method_def blocks when their mutator opesn. + * Called for mrc_steps blocks when their mutator opesn. * Triggers a flyout update and adds an event listener to the mutator workspace. * * @param block The block whose mutator is open. @@ -193,9 +196,9 @@ export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIc return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block); } -export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg { +export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): StepContainerBlock { // First create the container block. - const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg; + const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as StepContainerBlock; containerBlock.initSvg(); // Then add one step item block for each step. diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 71aa3cc7..873b67b1 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -25,20 +25,20 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; -import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; +import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' -import * as value from './utils/value'; +import { createBooleanShadowValue } from './utils/value'; import * as toolboxItems from '../toolbox/items'; export const BLOCK_NAME = 'mrc_steps'; const INPUT_CONDITION_PREFIX = 'CONDITION_'; -const INPUT_STEP_PREFIX = 'STEP_'; +const INPUT_STATEMENT_PREFIX = 'STATEMENT_'; /** Extra state for serialising mrc_steps blocks. */ type StepsExtraState = { /** - * The steps + * The step names. */ stepNames: string[], }; @@ -60,7 +60,6 @@ const STEPS = { this.setInputsInline(false); this.setStyle(MRC_STYLE_STEPS); this.setMutator(stepContainer.getMutatorIcon(this)); - this.updateShape_(); }, saveExtraState: function (this: StepsBlock): StepsExtraState { return { @@ -71,65 +70,88 @@ const STEPS = { this.mrcStepNames = state.stepNames; this.updateShape_(); }, - compose: function (this: StepsBlock, containerBlock: Blockly.Block) { - if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { - throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); + /** + * Populate the mutator's dialog with this block's components. + */ + decompose: function (this: StepsBlock, workspace: Blockly.Workspace): stepContainer.StepContainerBlock { + const stepNames: string[] = []; + this.mrcStepNames.forEach(step => { + stepNames.push(step); + }); + return stepContainer.createMutatorBlocks(workspace, stepNames); + }, + /** + * Store condition and statement connections on the StepItemBlocks + */ + saveConnections: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) { + const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks(); + for (let i = 0; i < stepItemBlocks.length; i++) { + const stepItemBlock = stepItemBlocks[i]; + const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); + stepItemBlock.conditionShadowState = + conditionInput && conditionInput.connection!.getShadowState(true); + stepItemBlock.conditionTargetConnection = + conditionInput && conditionInput.connection!.targetConnection; + const statementInput = this.getInput(INPUT_STATEMENT_PREFIX + i); + stepItemBlock.statementTargetConnection = + statementInput && statementInput.connection!.targetConnection; } - const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; - const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - + }, + /** + * Reconfigure this block based on the mutator dialog's components. + */ + compose: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) { + const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {}; + const conditionShadowStates: Array = []; + const conditionTargetConnections: Array = []; + const statementTargetConnections: Array = []; + + const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks(); + + // Iterate through the step item blocks to: + // - Update this.mrcStepNames + // - Keep track of steps that were renamed + // - Collect the condition and statement connections that were saved on the StepItemBlocks. this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { - this.mrcStepNames.push(stepItemBlock.getName()); - }); - - // Update jump blocks for any renamed steps - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - stepItemBlocks.forEach((stepItemBlock) => { - const oldName = stepItemBlock.getOriginalName(); - const newName = stepItemBlock.getName(); - if (oldName && oldName !== newName) { - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(newName, 'STEP_NAME'); - } - }); + const oldStepName = stepItemBlock.getOriginalName(); + const newStepName = stepItemBlock.getName(); + stepItemBlock.setOriginalName(newStepName); + this.mrcStepNames.push(newStepName); + if (oldStepName !== newStepName) { + mapOldStepNameToNewStepName[oldStepName] = newStepName; } + conditionShadowStates.push(stepItemBlock.conditionShadowState); + conditionTargetConnections.push(stepItemBlock.conditionTargetConnection as Blockly.Connection | null); + statementTargetConnections.push(stepItemBlock.statementTargetConnection as Blockly.Connection | null); }); this.updateShape_(); - // Add a shadow True block to each empty condition input. - for (var i = 0; i < this.mrcStepNames.length; i++) { + // Reconnect blocks. + for (let i = 0; i < this.mrcStepNames.length; i++) { + // Reconnect the condition. + conditionTargetConnections[i]?.reconnect(this, INPUT_CONDITION_PREFIX + i); + // Add the boolean shadow block to the condition input. This must be done after the condition + // has been reconnected. If it is done before the condition is reconnected, the shadow will + // become disconnected. + const conditionShadowState = conditionShadowStates[i] || createBooleanShadowValue(true).shadow; const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); - if (conditionInput && !conditionInput.connection?.targetConnection) { - const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; - shadowBlock.setShadow(true); - shadowBlock.setFieldValue('TRUE', 'BOOL'); - if (this.workspace.rendered) { - shadowBlock.initSvg(); - shadowBlock.render(); - } - conditionInput.connection?.connect(shadowBlock.outputConnection!); - } + conditionInput?.connection?.setShadowState(conditionShadowState as any); + // Reconnect the statement. + statementTargetConnections[i]?.reconnect(this, INPUT_STATEMENT_PREFIX + i); + } + + if (Object.keys(mapOldStepNameToNewStepName).length) { + // Update jump blocks for any renamed steps. + updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName); } - }, - decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { - const stepNames: string[] = []; - this.mrcStepNames.forEach(step => { - stepNames.push(step); - }); - return stepContainer.createMutatorBlocks(workspace, stepNames); }, /** - * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + * mrcOnMutatorOpen is called when the mutator on an StepsBlock is opened. */ mrcOnMutatorOpen: function (this: StepsBlock): void { stepContainer.onMutatorOpen(this); - }, - mrcOnChange: function (this: StepsBlock): void { - }, mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { const oldName = this.mrcStepNames[step]; @@ -152,99 +174,31 @@ const STEPS = { } this.mrcStepNames[step] = currentName; - // Update all mrc_jump_to_step blocks that reference the old name if (oldName !== currentName) { - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(currentName, 'STEP_NAME'); - } - }); + // Update all mrc_jump_to_step blocks that reference the old name + const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {}; + mapOldStepNameToNewStepName[oldName] = currentName; + updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName); } return currentName; }, updateShape_: function (this: StepsBlock): void { - // Build a map of step names to their current input indices - const currentStepMap: { [stepName: string]: number } = {}; - let i = 0; - while (this.getInput(INPUT_CONDITION_PREFIX + i)) { - const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); - const field = conditionInput?.fieldRow[0]; - if (field) { - currentStepMap[field.getValue()] = i; - } - i++; - } - - // For each new step position, find where it currently is (if it exists) - for (let j = 0; j < this.mrcStepNames.length; j++) { - const stepName = this.mrcStepNames[j]; - const currentIndex = currentStepMap[stepName]; - - if (currentIndex !== undefined && currentIndex !== j) { - // Step exists but is at wrong position - move it - const conditionConnection = this.getInput(INPUT_CONDITION_PREFIX + currentIndex)?.connection?.targetConnection; - const stepConnection = this.getInput(INPUT_STEP_PREFIX + currentIndex)?.connection?.targetConnection; - - // Temporarily disconnect - if (conditionConnection) { - conditionConnection.disconnect(); - } - if (stepConnection) { - stepConnection.disconnect(); - } - - // Remove old inputs - this.removeInput(INPUT_CONDITION_PREFIX + currentIndex, false); - this.removeInput(INPUT_STEP_PREFIX + currentIndex, false); - - // Create new inputs at correct position - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - this.appendValueInput(INPUT_CONDITION_PREFIX + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput(INPUT_STEP_PREFIX + j); - - // Reconnect - if (conditionConnection) { - this.getInput(INPUT_CONDITION_PREFIX + j)?.connection?.connect(conditionConnection); - } - if (stepConnection) { - this.getInput(INPUT_STEP_PREFIX + j)?.connection?.connect(stepConnection); - } - - delete currentStepMap[stepName]; - } else if (currentIndex !== undefined) { - // Step is at correct position - just update the field - const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + j); - const field = conditionInput?.fieldRow[0]; - if (field && field.getValue() !== stepName) { - field.setValue(stepName); - } - delete currentStepMap[stepName]; - } else { - // Step doesn't exist - create it - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - this.appendValueInput(INPUT_CONDITION_PREFIX + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput(INPUT_STEP_PREFIX + j); - } + // Remove all inputs. + for (let i = 0; this.getInput(INPUT_CONDITION_PREFIX + i); i++) { + this.removeInput(INPUT_CONDITION_PREFIX + i); + this.removeInput(INPUT_STATEMENT_PREFIX + i); } - - // Remove any leftover inputs (steps that were deleted) - for (const stepName in currentStepMap) { - const index = currentStepMap[stepName]; - this.removeInput(INPUT_CONDITION_PREFIX + index, false); - this.removeInput(INPUT_STEP_PREFIX + index, false); + // Add inputs for each step. + for (let i = 0; i < this.mrcStepNames.length; i++) { + const stepName = this.mrcStepNames[i]; + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, i)); + this.appendValueInput(INPUT_CONDITION_PREFIX + i) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput(INPUT_STATEMENT_PREFIX + i); } }, mrcGetStepNames: function (this: StepsBlock): string[] { @@ -276,11 +230,11 @@ export const pythonFromBlock = function ( code += generator.INDENT + 'match self._current_step:\n'; block.mrcStepNames.forEach((stepName, index) => { code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; - let stepCode = generator.statementToCode(block, INPUT_STEP_PREFIX + index); + const stepCode = generator.statementToCode(block, INPUT_STATEMENT_PREFIX + index); if (stepCode !== '') { code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); } - let conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False'; + const conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False'; code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; if (index === block.mrcStepNames.length - 1) { code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; @@ -300,6 +254,6 @@ export function createStepsBlock(): toolboxItems.Block { }; const fields: {[key: string]: any} = {}; const inputs: {[key: string]: any} = {}; - inputs[INPUT_CONDITION_PREFIX + 0] = value.createBooleanShadowValue(true); + inputs[INPUT_CONDITION_PREFIX + 0] = createBooleanShadowValue(true); return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); } From 7ef758348a3958f0ddc44b08fc55c88c3dfd755b Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Tue, 4 Nov 2025 19:58:53 -0800 Subject: [PATCH 31/34] Added STEPS and REPEAT_UNTIL strings to Hebrew translation file. --- src/i18n/locales/he/translation.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 061c0fd7..0c6d7821 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -148,6 +148,8 @@ "GET": "קבל", "SET": "הגדר", "TO": "ל", + "STEPS": "צעדים", + "REPEAT_UNTIL": "לחזור על כך עד", "CUSTOM_EVENTS_LABEL": "אירועים מותאמים אישית", "CUSTOM_METHODS_LABEL": "מתודות מותאמות אישית", "MORE_ROBOT_METHODS_LABEL": "מתודות נוספות לרובוט", From 291cd02d6d4833e142079b2551ecc90a30ed1f3e Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Tue, 4 Nov 2025 20:39:57 -0800 Subject: [PATCH 32/34] Added strings for mrc_jump_to_step blocks to i18n. --- src/blocks/mrc_jump_to_step.ts | 9 +++++++-- src/blocks/tokens.ts | 2 ++ src/i18n/locales/en/translation.json | 2 ++ src/i18n/locales/es/translation.json | 2 ++ src/i18n/locales/he/translation.json | 2 ++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 50f940ff..ca7cb999 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -47,12 +47,17 @@ const JUMP_TO_STEP_BLOCK = { */ init: function (this: JumpToStepBlock): void { this.appendDummyInput() - .appendField('Jump to') + .appendField(Blockly.Msg.JUMP_TO) .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); this.setPreviousStatement(true, null); this.setInputsInline(true); this.setStyle(MRC_STYLE_VARIABLES); - this.setTooltip('Jump to the specified step.'); + this.setTooltip(() => { + const stepName = this.getFieldValue(FIELD_STEP_NAME); + let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP; + tooltip = tooltip.replace('{{stepName}}', stepName); + return tooltip; + }); }, /** * mrcOnMove is called when an EventBlock is moved. diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 0be5fc91..bcc7109f 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -131,6 +131,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { COMMENT_DEFAULT_TEXT: t('BLOCKLY.COMMENT_DEFAULT_TEXT'), STEPS: t('BLOCKLY.STEPS'), REPEAT_UNTIL: t('BLOCKLY.REPEAT_UNTIL'), + JUMP_TO: t('BLOCKLY.JUMP_TO'), + JUMP_TO_STEP_TOOLTIP: t('BLOCKLY.TOOLTIP.JUMP_TO_STEP'), JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK: t('BLOCKLY.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK'), } }; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 6f4e4f74..668e3647 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -150,6 +150,7 @@ "TO": "to", "STEPS": "steps", "REPEAT_UNTIL": "repeat until", + "JUMP_TO": "jump to", "CUSTOM_EVENTS_LABEL": "Custom Events", "CUSTOM_METHODS_LABEL": "Custom Methods", "MORE_ROBOT_METHODS_LABEL": "More Robot Methods", @@ -165,6 +166,7 @@ "OPMODE_GROUP": "An optional group to group OpModes on Driver Station", "COMPONENTS": "These components are visible in this mechanism, the robot, and all opmodes.", "PRIVATE_COMPONENTS": "These components will not be visible in the robot or opmodes. They are only accessible within this mechanism.", + "JUMP_TO_STEP": "Jump to the step named {{stepName}}.", "CALL_BUILTIN_FUNCTION": "Calls the builtin function {{functionName}}.", "CALL_MODULE_FUNCTION": "Calls the module function {{moduleName}}.{{functionName}}.", "CALL_STATIC_METHOD": "Calls the static method {{className}}.{{functionName}}.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 3f568ae5..907bfa43 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -151,6 +151,7 @@ "TO": "a", "STEPS": "pasos", "REPEAT_UNTIL": "repetir hasta", + "JUMP_TO": "saltar a", "CUSTOM_EVENTS_LABEL": "Eventos Personalizados", "CUSTOM_METHODS_LABEL": "Métodos Personalizados", "MORE_ROBOT_METHODS_LABEL": "Más Métodos del Robot", @@ -166,6 +167,7 @@ "OPMODE_GROUP": "Un grupo opcional para agrupar OpModes en la Estación del Conductor", "COMPONENTS": "Estos componentes son visibles en este mecanismo, el robot y todos los opmodes.", "PRIVATE_COMPONENTS": "Estos componentes no serán visibles en el robot o en los opmodes. Solo son accesibles dentro de este mecanismo.", + "JUMP_TO_STEP": "Saltar al paso llamado {{stepName}}.", "CALL_BUILTIN_FUNCTION": "Llama a la función incorporada {{functionName}}.", "CALL_MODULE_FUNCTION": "Llama a la función del módulo {{moduleName}}.{{functionName}}.", "CALL_STATIC_METHOD": "Llama al método estático {{className}}.{{functionName}}.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 0c6d7821..56f6e55d 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -150,6 +150,7 @@ "TO": "ל", "STEPS": "צעדים", "REPEAT_UNTIL": "לחזור על כך עד", + "JUMP_TO": "לקפוץ אל", "CUSTOM_EVENTS_LABEL": "אירועים מותאמים אישית", "CUSTOM_METHODS_LABEL": "מתודות מותאמות אישית", "MORE_ROBOT_METHODS_LABEL": "מתודות נוספות לרובוט", @@ -165,6 +166,7 @@ "OPMODE_GROUP": "קבוצה אופציונלית לארגון אופמודים באפליקציית ה־Driver Station", "COMPONENTS": "הרכיבים האלה גלויים במנגנון הזה, ברובוט ובכל האופמודים.", "PRIVATE_COMPONENTS": "הרכיבים האלה לא יהיו גלויים ברובוט או באופמודים. הם נגישים רק בתוך המנגנון הזה.", + "JUMP_TO_STEP": "קפוץ לשלב בשם {{stepName}}.", "CALL_BUILTIN_FUNCTION": "קורא לפונקציה מובנית בשם {{functionName}}.", "CALL_MODULE_FUNCTION": "קורא לפונקציה {{functionName}} במודול {{moduleName}}.", "CALL_STATIC_METHOD": "קורא למתודה סטטית {{className}}.{{functionName}}.", From 42564a5bee3e685992b30f63cfab904f6838ef8e Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Tue, 4 Nov 2025 20:45:08 -0800 Subject: [PATCH 33/34] Fixed inaccurate comments. --- src/blocks/mrc_jump_to_step.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index ca7cb999..4bf409e1 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -60,8 +60,8 @@ const JUMP_TO_STEP_BLOCK = { }); }, /** - * mrcOnMove is called when an EventBlock is moved. - */ + * mrcOnMove is called when a JumpToStepBlock is moved. + */ mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void { this.checkBlockPlacement(); }, @@ -73,14 +73,14 @@ const JUMP_TO_STEP_BLOCK = { const rootBlock: Blockly.Block | null = this.getRootBlock(); if (rootBlock.type === MRC_STEPS) { - // This block is within a class method definition. + // This block is within a steps block. const stepsBlock = rootBlock as StepsBlock; - // Add the method's parameter names to legalStepNames. + // Add the step names to legalStepNames. legalStepNames.push(...stepsBlock.mrcGetStepNames()); } if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { - // If this blocks's parameter name is in legalParameterNames, it's good. + // If this blocks's step name is in legalStepNames, it's good. this.setWarningText(null, WARNING_ID_NOT_IN_STEP); this.mrcHasWarning = false; } else { From f51f3a9de2b7e0947a1948cd274233ca7f96f50f Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Tue, 4 Nov 2025 20:52:40 -0800 Subject: [PATCH 34/34] Renamed createAdvanceToBlock to createJumpToStepBlock. Renamed paramName (createStepFieldFlydown parameter) to stepName. Removed export from createParameterBlock and createJumpToStepBlock. --- src/fields/field_flydown.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index 5b2dcac8..f178d7ae 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -373,7 +373,7 @@ export class FieldFlydown extends Blockly.FieldTextInput { } } -export function createParameterBlock(paramName: string): Blockly.utils.toolbox.FlyoutDefinition { +function createParameterBlock(paramName: string): Blockly.utils.toolbox.FlyoutDefinition { return { contents: [ { @@ -387,7 +387,7 @@ export function createParameterBlock(paramName: string): Blockly.utils.toolbox.F }; } -export function createAdvanceToBlock(stepName: string): Blockly.utils.toolbox.FlyoutDefinition { +function createJumpToStepBlock(stepName: string): Blockly.utils.toolbox.FlyoutDefinition { return { contents: [ { @@ -413,6 +413,6 @@ export function createParameterFieldFlydown(paramName: string, isEditable: boole return new FieldFlydown(paramName, isEditable, createParameterBlock); } -export function createStepFieldFlydown(paramName: string, isEditable: boolean): Blockly.Field { - return new FieldFlydown(paramName, isEditable, createAdvanceToBlock); +export function createStepFieldFlydown(stepName: string, isEditable: boolean): Blockly.Field { + return new FieldFlydown(stepName, isEditable, createJumpToStepBlock); } \ No newline at end of file