Skip to content

Commit 2523d8a

Browse files
alan412lizlooney
andauthored
Pr try steps block (#291)
* For rough prototype * Split step into a label and field * Change to allow field flydown for more than params * Add advance to step blocks * use flydown * First bit of having steps * Make default starting out with one step * Change from Advance to Jump * beginning of generating code * Limit Jump to step to be within steps * Move steps into opmode and only show when not already in opmode * Call steps as part of loop if it is defined * Clean up warnings * Ran format on new files * Simplify python generation * Simplify python generation * Updates jump correctly * remove warnings * fix typo * add Shadow true blocks * Bump version to 0.0.4 * Change case of Repeat Until * make sure self.steps is callable * Change to 2 space indentation * change to 2 space indentation * Addressed review comments * 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. * Added constants for INPUT_CONDITION_PREFIX and INPUT_STEP_PREFIX. * Moved code to add shadow True blocks from updateShape_ to compose. * 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.) * Added STEPS and REPEAT_UNTIL strings to Hebrew translation file. * Added strings for mrc_jump_to_step blocks to i18n. * Fixed inaccurate comments. * Renamed createAdvanceToBlock to createJumpToStepBlock. Renamed paramName (createStepFieldFlydown parameter) to stepName. Removed export from createParameterBlock and createJumpToStepBlock. --------- Co-authored-by: Liz Looney <lizlooney@gmail.com>
1 parent 926d78d commit 2523d8a

File tree

16 files changed

+719
-25
lines changed

16 files changed

+719
-25
lines changed

server_python_scripts/blocks_base_classes/opmode.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ def __init__(self, robot: RobotBase):
77
def start(self) -> None:
88
self.robot.start()
99
def loop(self) -> None:
10+
# Call steps method if it exists in the derived class
11+
if hasattr(self, 'steps') and callable(self.steps):
12+
self.steps()
1013
self.robot.update()
1114
def stop(self) -> None:
1215
self.robot.stop()

src/blocks/mrc_class_method_def.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import * as Blockly from 'blockly';
2323
import { MRC_STYLE_FUNCTIONS } from '../themes/styles';
2424
import { createFieldNonEditableText } from '../fields/FieldNonEditableText'
25-
import { createFieldFlydown } from '../fields/field_flydown';
25+
import { createParameterFieldFlydown } from '../fields/field_flydown';
2626
import { Order } from 'blockly/python';
2727
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2828
import * as storageModule from '../storage/module';
@@ -62,7 +62,7 @@ interface ClassMethodDefMixin extends ClassMethodDefMixinType {
6262
}
6363
type ClassMethodDefMixinType = typeof CLASS_METHOD_DEF;
6464

65-
/** Extra state for serialising call_python_* blocks. */
65+
/** Extra state for serialising mrc_class_method_def blocks. */
6666
type ClassMethodDefExtraState = {
6767
/**
6868
* The id that identifies this method definition.
@@ -241,7 +241,7 @@ const CLASS_METHOD_DEF = {
241241
this.removeParameterFields(input);
242242
this.mrcParameters.forEach((param) => {
243243
const paramName = FIELD_PARAM_PREFIX + param.name;
244-
input.appendField(createFieldFlydown(param.name, false), paramName);
244+
input.appendField(createParameterFieldFlydown(param.name, false), paramName);
245245
});
246246
}
247247
}

src/blocks/mrc_event_handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import type { MessageInstance } from 'antd/es/message/interface';
2727
import { Parameter } from './mrc_class_method_def';
2828
import { Editor } from '../editor/editor';
2929
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
30-
import { createFieldFlydown } from '../fields/field_flydown';
30+
import { createParameterFieldFlydown } from '../fields/field_flydown';
3131
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
3232
import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles';
3333
import * as toolboxItems from '../toolbox/items';
@@ -146,7 +146,7 @@ const EVENT_HANDLER = {
146146
this.removeParameterFields(input);
147147
this.mrcParameters.forEach((param) => {
148148
const paramName = `PARAM_${param.name}`;
149-
input.appendField(createFieldFlydown(param.name, false), paramName);
149+
input.appendField(createParameterFieldFlydown(param.name, false), paramName);
150150
});
151151
}
152152
} else {

src/blocks/mrc_jump_to_step.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Porpoiseful LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @fileoverview This is a block that allows your code to jump to a specific step.
20+
* @author alan@porpoiseful.com (Alan Smith)
21+
*/
22+
import * as Blockly from 'blockly';
23+
24+
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
25+
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
26+
import { MRC_STYLE_VARIABLES } from '../themes/styles';
27+
import { BLOCK_NAME as MRC_STEPS, StepsBlock } from './mrc_steps'
28+
29+
export const BLOCK_NAME = 'mrc_jump_to_step';
30+
31+
const FIELD_STEP_NAME = 'STEP_NAME';
32+
33+
const WARNING_ID_NOT_IN_STEP = 'not in step';
34+
35+
36+
type JumpToStepBlock = Blockly.Block & Blockly.BlockSvg & JumpToStepMixin;
37+
38+
interface JumpToStepMixin extends JumpToStepMixinType {
39+
mrcHasWarning: boolean,
40+
}
41+
42+
type JumpToStepMixinType = typeof JUMP_TO_STEP_BLOCK;
43+
44+
const JUMP_TO_STEP_BLOCK = {
45+
/**
46+
* Block initialization.
47+
*/
48+
init: function (this: JumpToStepBlock): void {
49+
this.appendDummyInput()
50+
.appendField(Blockly.Msg.JUMP_TO)
51+
.appendField(createFieldNonEditableText(''), FIELD_STEP_NAME);
52+
this.setPreviousStatement(true, null);
53+
this.setInputsInline(true);
54+
this.setStyle(MRC_STYLE_VARIABLES);
55+
this.setTooltip(() => {
56+
const stepName = this.getFieldValue(FIELD_STEP_NAME);
57+
let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP;
58+
tooltip = tooltip.replace('{{stepName}}', stepName);
59+
return tooltip;
60+
});
61+
},
62+
/**
63+
* mrcOnMove is called when a JumpToStepBlock is moved.
64+
*/
65+
mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void {
66+
this.checkBlockPlacement();
67+
},
68+
mrcOnAncestorMove: function (this: JumpToStepBlock): void {
69+
this.checkBlockPlacement();
70+
},
71+
checkBlockPlacement: function (this: JumpToStepBlock): void {
72+
const legalStepNames: string[] = [];
73+
74+
const rootBlock: Blockly.Block | null = this.getRootBlock();
75+
if (rootBlock.type === MRC_STEPS) {
76+
// This block is within a steps block.
77+
const stepsBlock = rootBlock as StepsBlock;
78+
// Add the step names to legalStepNames.
79+
legalStepNames.push(...stepsBlock.mrcGetStepNames());
80+
}
81+
82+
if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) {
83+
// If this blocks's step name is in legalStepNames, it's good.
84+
this.setWarningText(null, WARNING_ID_NOT_IN_STEP);
85+
this.mrcHasWarning = false;
86+
} else {
87+
// Otherwise, add a warning to this block.
88+
if (!this.mrcHasWarning) {
89+
this.setWarningText(Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK, WARNING_ID_NOT_IN_STEP);
90+
this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true);
91+
this.mrcHasWarning = true;
92+
}
93+
}
94+
},
95+
};
96+
97+
export const setup = function () {
98+
Blockly.Blocks[BLOCK_NAME] = JUMP_TO_STEP_BLOCK;
99+
};
100+
101+
export const pythonFromBlock = function (
102+
block: JumpToStepBlock,
103+
_generator: ExtendedPythonGenerator,
104+
) {
105+
let code = 'self._current_step = "' +
106+
block.getFieldValue(FIELD_STEP_NAME) + '"\n';
107+
code += 'return\n';
108+
109+
return code;
110+
};
111+
112+
export function renameSteps(workspace: Blockly.Workspace, mapOldStepNameToNewStepName: {[newStepName: string]: string}): void {
113+
workspace.getBlocksByType(BLOCK_NAME, false).forEach((jumpBlock) => {
114+
const stepName = jumpBlock.getFieldValue(FIELD_STEP_NAME);
115+
if (stepName in mapOldStepNameToNewStepName) {
116+
const newStepName = mapOldStepNameToNewStepName[stepName];
117+
jumpBlock.setFieldValue(newStepName, FIELD_STEP_NAME);
118+
}
119+
});
120+
}

src/blocks/mrc_step_container.ts

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Porpoiseful LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @fileoverview Mutator for steps.
20+
* @author alan@porpoiseful.com (Alan Smith)
21+
*/
22+
import * as Blockly from 'blockly';
23+
import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles';
24+
25+
export const STEP_CONTAINER_BLOCK_NAME = 'mrc_step_container';
26+
const STEP_ITEM_BLOCK_NAME = 'mrc_step_item';
27+
28+
export const setup = function () {
29+
Blockly.Blocks[STEP_CONTAINER_BLOCK_NAME] = STEP_CONTAINER;
30+
Blockly.Blocks[STEP_ITEM_BLOCK_NAME] = STEP_ITEM;
31+
};
32+
33+
// The step container block.
34+
35+
const INPUT_STACK = 'STACK';
36+
37+
export type StepContainerBlock = StepContainerMixin & Blockly.BlockSvg;
38+
interface StepContainerMixin extends StepContainerMixinType { }
39+
type StepContainerMixinType = typeof STEP_CONTAINER;
40+
41+
const STEP_CONTAINER = {
42+
init: function (this: StepContainerBlock) {
43+
this.appendDummyInput().appendField(Blockly.Msg.STEPS);
44+
this.appendStatementInput(INPUT_STACK);
45+
this.setStyle(MRC_STYLE_CLASS_BLOCKS);
46+
this.contextMenu = false;
47+
},
48+
getStepItemBlocks: function (this: StepContainerBlock): StepItemBlock[] {
49+
const stepItemBlocks: StepItemBlock[] = [];
50+
let block = this.getInputTargetBlock(INPUT_STACK);
51+
while (block && !block.isInsertionMarker()) {
52+
if (block.type !== STEP_ITEM_BLOCK_NAME) {
53+
throw new Error('getItemNames: block.type should be ' + STEP_ITEM_BLOCK_NAME);
54+
}
55+
stepItemBlocks.push(block as StepItemBlock);
56+
block = block.nextConnection && block.nextConnection.targetBlock();
57+
}
58+
return stepItemBlocks;
59+
},
60+
};
61+
62+
// The step item block.
63+
64+
const FIELD_NAME = 'NAME';
65+
66+
export type StepItemBlock = StepItemMixin & Blockly.BlockSvg;
67+
interface StepItemMixin extends StepItemMixinType {
68+
originalName: string,
69+
conditionShadowState?: any;
70+
conditionTargetConnection?: Blockly.Connection | null;
71+
statementTargetConnection?: Blockly.Connection | null;
72+
}
73+
74+
type StepItemMixinType = typeof STEP_ITEM;
75+
76+
const STEP_ITEM = {
77+
init: function (this: StepItemBlock) {
78+
this.appendDummyInput()
79+
.appendField(new Blockly.FieldTextInput(''), FIELD_NAME);
80+
this.setPreviousStatement(true);
81+
this.setNextStatement(true);
82+
this.setStyle(MRC_STYLE_CLASS_BLOCKS);
83+
this.originalName = '';
84+
this.contextMenu = false;
85+
},
86+
makeNameLegal: function (this: StepItemBlock): void {
87+
const rootBlock: Blockly.Block | null = this.getRootBlock();
88+
if (rootBlock) {
89+
const otherNames: string[] = []
90+
rootBlock!.getDescendants(true)?.forEach(itemBlock => {
91+
if (itemBlock != this) {
92+
otherNames.push(itemBlock.getFieldValue(FIELD_NAME));
93+
}
94+
});
95+
let currentName = this.getFieldValue(FIELD_NAME);
96+
while (otherNames.includes(currentName)) {
97+
// Check if currentName ends with a number
98+
const match = currentName.match(/^(.*?)(\d+)$/);
99+
if (match) {
100+
// If it ends with a number, increment it
101+
const baseName = match[1];
102+
const number = parseInt(match[2], 10);
103+
currentName = baseName + (number + 1);
104+
} else {
105+
// If it doesn't end with a number, append 2
106+
currentName = currentName + '2';
107+
}
108+
}
109+
this.setFieldValue(currentName, FIELD_NAME);
110+
updateMutatorFlyout(this.workspace);
111+
}
112+
},
113+
getName: function (this: StepItemBlock): string {
114+
return this.getFieldValue(FIELD_NAME);
115+
},
116+
getOriginalName: function (this: StepItemBlock): string {
117+
return this.originalName;
118+
},
119+
setOriginalName: function (this: StepItemBlock, originalName: string): void {
120+
this.originalName = originalName;
121+
},
122+
}
123+
124+
/**
125+
* Updates the mutator's flyout so that it contains a single step item block
126+
* whose name is not a duplicate of an existing step item.
127+
*
128+
* @param workspace The mutator's workspace. This workspace's flyout is what is being updated.
129+
*/
130+
function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) {
131+
const usedNames: string[] = [];
132+
workspace.getBlocksByType(STEP_ITEM_BLOCK_NAME, false).forEach(block => {
133+
usedNames.push(block.getFieldValue(FIELD_NAME));
134+
});
135+
136+
// Find the first unused number starting from 0
137+
let counter = 0;
138+
let uniqueName = counter.toString();
139+
while (usedNames.includes(uniqueName)) {
140+
counter++;
141+
uniqueName = counter.toString();
142+
}
143+
144+
const jsonBlock = {
145+
kind: 'block',
146+
type: STEP_ITEM_BLOCK_NAME,
147+
fields: {
148+
NAME: uniqueName,
149+
},
150+
};
151+
152+
workspace.updateToolbox({ contents: [jsonBlock] });
153+
}
154+
155+
/**
156+
* The blockly event listener function for the mutator's workspace.
157+
*/
158+
function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abstract) {
159+
if (event.type === Blockly.Events.BLOCK_MOVE) {
160+
const blockMoveEvent = event as Blockly.Events.BlockMove;
161+
const reason: string[] = blockMoveEvent.reason ?? [];
162+
if (reason.includes('connect') && blockMoveEvent.blockId) {
163+
const block = mutatorWorkspace.getBlockById(blockMoveEvent.blockId);
164+
if (block && block.type === STEP_ITEM_BLOCK_NAME) {
165+
(block as StepItemBlock).makeNameLegal();
166+
}
167+
}
168+
} else if (event.type === Blockly.Events.BLOCK_CHANGE) {
169+
const blockChangeEvent = event as Blockly.Events.BlockChange;
170+
if (blockChangeEvent.blockId) {
171+
const block = mutatorWorkspace.getBlockById(blockChangeEvent.blockId);
172+
if (block && block.type === STEP_ITEM_BLOCK_NAME) {
173+
(block as StepItemBlock).makeNameLegal();
174+
}
175+
}
176+
}
177+
}
178+
179+
/**
180+
* Called for mrc_steps blocks when their mutator opesn.
181+
* Triggers a flyout update and adds an event listener to the mutator workspace.
182+
*
183+
* @param block The block whose mutator is open.
184+
*/
185+
export function onMutatorOpen(block: Blockly.BlockSvg) {
186+
const mutatorIcon = block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon;
187+
const mutatorWorkspace = mutatorIcon.getWorkspace()!;
188+
updateMutatorFlyout(mutatorWorkspace);
189+
mutatorWorkspace.addChangeListener(event => onChange(mutatorWorkspace, event));
190+
}
191+
192+
/**
193+
* Returns the MutatorIcon for the given block.
194+
*/
195+
export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIcon {
196+
return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block);
197+
}
198+
199+
export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): StepContainerBlock {
200+
// First create the container block.
201+
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as StepContainerBlock;
202+
containerBlock.initSvg();
203+
204+
// Then add one step item block for each step.
205+
let connection = containerBlock!.getInput(INPUT_STACK)!.connection;
206+
for (const stepName of stepNames) {
207+
const itemBlock = workspace.newBlock(STEP_ITEM_BLOCK_NAME) as StepItemBlock;
208+
itemBlock.initSvg();
209+
itemBlock.setFieldValue(stepName, FIELD_NAME);
210+
itemBlock.originalName = stepName;
211+
connection!.connect(itemBlock.previousConnection!);
212+
connection = itemBlock.nextConnection;
213+
}
214+
return containerBlock;
215+
}

0 commit comments

Comments
 (0)