Skip to content

Commit 200b773

Browse files
committed
First bit of having steps
1 parent 5a3e824 commit 200b773

File tree

7 files changed

+287
-36
lines changed

7 files changed

+287
-36
lines changed

src/blocks/mrc_step_container.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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 [email protected] (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+
}
70+
71+
type StepItemMixinType = typeof STEP_ITEM;
72+
73+
const STEP_ITEM = {
74+
init: function (this: StepItemBlock) {
75+
this.appendDummyInput()
76+
.appendField(new Blockly.FieldTextInput(''), FIELD_NAME);
77+
this.setPreviousStatement(true);
78+
this.setNextStatement(true);
79+
this.setStyle(MRC_STYLE_CLASS_BLOCKS);
80+
this.originalName = '';
81+
this.contextMenu = false;
82+
},
83+
makeNameLegal: function (this: StepItemBlock): void {
84+
const rootBlock: Blockly.Block | null = this.getRootBlock();
85+
if (rootBlock) {
86+
const otherNames: string[] = []
87+
rootBlock!.getDescendants(true)?.forEach(itemBlock => {
88+
if (itemBlock != this) {
89+
otherNames.push(itemBlock.getFieldValue(FIELD_NAME));
90+
}
91+
});
92+
let currentName = this.getFieldValue(FIELD_NAME);
93+
while (otherNames.includes(currentName)) {
94+
// Check if currentName ends with a number
95+
const match = currentName.match(/^(.*?)(\d+)$/);
96+
if (match) {
97+
// If it ends with a number, increment it
98+
const baseName = match[1];
99+
const number = parseInt(match[2], 10);
100+
currentName = baseName + (number + 1);
101+
} else {
102+
// If it doesn't end with a number, append 2
103+
currentName = currentName + '2';
104+
}
105+
}
106+
this.setFieldValue(currentName, FIELD_NAME);
107+
updateMutatorFlyout(this.workspace);
108+
}
109+
},
110+
getName: function (this: StepItemBlock): string {
111+
return this.getFieldValue(FIELD_NAME);
112+
},
113+
getOriginalName: function (this: StepItemBlock): string {
114+
return this.originalName;
115+
},
116+
setOriginalName: function (this: StepItemBlock, originalName: string): void {
117+
this.originalName = originalName;
118+
},
119+
}
120+
121+
/**
122+
* Updates the mutator's flyout so that it contains a single step item block
123+
* whose name is not a duplicate of an existing step item.
124+
*
125+
* @param workspace The mutator's workspace. This workspace's flyout is what is being updated.
126+
*/
127+
function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) {
128+
const usedNames: string[] = [];
129+
workspace.getBlocksByType(STEP_ITEM_BLOCK_NAME, false).forEach(block => {
130+
usedNames.push(block.getFieldValue(FIELD_NAME));
131+
});
132+
133+
// Find the first unused number starting from 0
134+
let counter = 0;
135+
let uniqueName = counter.toString();
136+
while (usedNames.includes(uniqueName)) {
137+
counter++;
138+
uniqueName = counter.toString();
139+
}
140+
141+
const jsonBlock = {
142+
kind: 'block',
143+
type: STEP_ITEM_BLOCK_NAME,
144+
fields: {
145+
NAME: uniqueName,
146+
},
147+
};
148+
149+
workspace.updateToolbox({ contents: [jsonBlock] });
150+
}
151+
152+
/**
153+
* The blockly event listener function for the mutator's workspace.
154+
*/
155+
function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abstract) {
156+
if (event.type === Blockly.Events.BLOCK_MOVE) {
157+
const blockMoveEvent = event as Blockly.Events.BlockMove;
158+
const reason: string[] = blockMoveEvent.reason ?? [];
159+
if (reason.includes('connect') && blockMoveEvent.blockId) {
160+
const block = mutatorWorkspace.getBlockById(blockMoveEvent.blockId);
161+
if (block && block.type === STEP_ITEM_BLOCK_NAME) {
162+
(block as StepItemBlock).makeNameLegal();
163+
}
164+
}
165+
} else if (event.type === Blockly.Events.BLOCK_CHANGE) {
166+
const blockChangeEvent = event as Blockly.Events.BlockChange;
167+
if (blockChangeEvent.blockId) {
168+
const block = mutatorWorkspace.getBlockById(blockChangeEvent.blockId);
169+
if (block && block.type === STEP_ITEM_BLOCK_NAME) {
170+
(block as StepItemBlock).makeNameLegal();
171+
}
172+
}
173+
}
174+
}
175+
176+
/**
177+
* Called for mrc_event and mrc_class_method_def blocks when their mutator opesn.
178+
* Triggers a flyout update and adds an event listener to the mutator workspace.
179+
*
180+
* @param block The block whose mutator is open.
181+
*/
182+
export function onMutatorOpen(block: Blockly.BlockSvg) {
183+
const mutatorIcon = block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon;
184+
const mutatorWorkspace = mutatorIcon.getWorkspace()!;
185+
updateMutatorFlyout(mutatorWorkspace);
186+
mutatorWorkspace.addChangeListener(event => onChange(mutatorWorkspace, event));
187+
}
188+
189+
/**
190+
* Returns the MutatorIcon for the given block.
191+
*/
192+
export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIcon {
193+
return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block);
194+
}
195+
196+
export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg {
197+
// First create the container block.
198+
const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg;
199+
containerBlock.initSvg();
200+
201+
// Then add one step item block for each step.
202+
let connection = containerBlock!.getInput(INPUT_STACK)!.connection;
203+
for (const stepName of stepNames) {
204+
const itemBlock = workspace.newBlock(STEP_ITEM_BLOCK_NAME) as StepItemBlock;
205+
itemBlock.initSvg();
206+
itemBlock.setFieldValue(stepName, FIELD_NAME);
207+
itemBlock.originalName = stepName;
208+
connection!.connect(itemBlock.previousConnection!);
209+
connection = itemBlock.nextConnection;
210+
}
211+
return containerBlock;
212+
}

src/blocks/mrc_steps.ts

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@
2020
* @author [email protected] (Alan Smith)
2121
*/
2222
import * as Blockly from 'blockly';
23-
import { MRC_STYLE_FUNCTIONS } from '../themes/styles';
23+
import { MRC_STYLE_STEPS } from '../themes/styles';
2424
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
25-
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
2625
import { createStepFieldFlydown } from '../fields/field_flydown';
27-
import * as paramContainer from './mrc_param_container'
26+
import * as stepContainer from './mrc_step_container'
2827

2928
export const BLOCK_NAME = 'mrc_steps';
30-
const MUTATOR_BLOCK_NAME = 'steps_mutatorarg';
29+
// const MUTATOR_BLOCK_NAME = 'steps_mutatorarg';
3130

3231

3332
export type StepsBlock = Blockly.Block & StepsMixin & Blockly.BlockSvg;
3433
interface StepsMixin extends StepsMixinType {
34+
mrcStepNames: string[];
3535
}
3636
type StepsMixinType = typeof STEPS;
3737

@@ -40,47 +40,74 @@ const STEPS = {
4040
* Block initialization.
4141
*/
4242
init: function (this: StepsBlock): void {
43+
this.mrcStepNames = [];
4344
this.appendDummyInput()
44-
.appendField(createFieldNonEditableText('steps'));
45-
this.appendDummyInput()
46-
.appendField('Step')
47-
.appendField(createStepFieldFlydown('0', false));
48-
this.appendStatementInput('STEP_0');
49-
this.appendValueInput('CONDITION_0')
50-
.setCheck('Boolean')
51-
.appendField('Advance when');
52-
this.appendDummyInput()
53-
.appendField('Step')
54-
.appendField(createStepFieldFlydown('1', false));
55-
this.appendStatementInput('STEP_1');
56-
this.appendValueInput('CONDITION_1')
57-
.setCheck('Boolean')
58-
.appendField('Finish when');
45+
.appendField(Blockly.Msg.STEPS);
46+
/*
47+
this.appendValueInput('CONDITION_0')
48+
.appendField(createStepFieldFlydown('shoot', true))
49+
.setCheck('Boolean')
50+
.appendField('Repeat Until');
51+
this.appendStatementInput('STEP_0');
52+
53+
this.appendValueInput('CONDITION_1')
54+
.appendField(createStepFieldFlydown('move', true))
55+
.setCheck('Boolean')
56+
.appendField('Repeat Until');
57+
this.appendStatementInput('STEP_1');
58+
*/
5959
this.setInputsInline(false);
60-
this.setStyle(MRC_STYLE_FUNCTIONS);
61-
this.setMutator(paramContainer.getMutatorIcon(this));
62-
},
60+
this.setStyle(MRC_STYLE_STEPS);
61+
this.setMutator(stepContainer.getMutatorIcon(this));
62+
},
6363
compose: function (this: StepsBlock, containerBlock: Blockly.Block) {
64-
if (containerBlock.type !== paramContainer.PARAM_CONTAINER_BLOCK_NAME) {
65-
throw new Error('compose: containerBlock.type should be ' + paramContainer.PARAM_CONTAINER_BLOCK_NAME);
66-
}
67-
const paramContainerBlock = containerBlock as paramContainer.ParamContainerBlock;
68-
const paramItemBlocks: paramContainer.ParamItemBlock[] = paramContainerBlock.getParamItemBlocks();
69-
64+
if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) {
65+
throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME);
66+
}
67+
const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock;
68+
const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks();
69+
stepItemBlocks.forEach((stepItemBlock) => {
70+
});
71+
this.mrcStepNames = [];
72+
stepItemBlocks.forEach((stepItemBlock) => {
73+
this.mrcStepNames.push(stepItemBlock.getName());
74+
});
75+
// TODO: Update any jump blocks to have the correct name
76+
this.updateShape_();
7077
},
7178
decompose: function (this: StepsBlock, workspace: Blockly.Workspace) {
72-
const parameterNames: string[] = [];
73-
74-
return paramContainer.createMutatorBlocks(workspace, parameterNames);
79+
const stepNames: string[] = [];
80+
this.mrcStepNames.forEach(step => {
81+
stepNames.push(step);
82+
});
83+
return stepContainer.createMutatorBlocks(workspace, stepNames);
84+
},
85+
/**
86+
* mrcOnMutatorOpen is called when the mutator on an EventBlock is opened.
87+
*/
88+
mrcOnMutatorOpen: function(this: StepsBlock): void {
89+
stepContainer.onMutatorOpen(this);
90+
},
91+
updateShape_: function (this: StepsBlock): void {
92+
// some way of knowing what was there before and what is there now
93+
let success = true;
94+
let i = 0;
95+
while (success){
96+
success = this.removeInput('CONDITION_' + i, true);
97+
success = this.removeInput('STEP_' + i, true);
98+
i++;
99+
}
100+
for (let j = 0; j < this.mrcStepNames.length; j++) {
101+
this.appendValueInput('CONDITION_' + j)
102+
.appendField(createStepFieldFlydown(this.mrcStepNames[j], true))
103+
.setCheck('Boolean')
104+
.appendField(Blockly.Msg.REPEAT_UNTIL);
105+
this.appendStatementInput('STEP_' + j);
106+
}
75107
},
76-
};
77-
78-
const MUTATOR_STEPS = {
79-
80108
};
81109

82110
export const setup = function () {
83-
Blockly.Blocks[MUTATOR_BLOCK_NAME] = MUTATOR_STEPS;
84111
Blockly.Blocks[BLOCK_NAME] = STEPS;
85112
};
86113

src/blocks/setup_custom_blocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as ParamContainer from './mrc_param_container'
1919
import * as Port from './mrc_port';
2020
import * as SetPythonVariable from './mrc_set_python_variable';
2121
import * as Steps from './mrc_steps';
22+
import * as StepContainer from './mrc_step_container';
2223
import * as AdvanceToStep from './mrc_advance_to_step';
2324

2425
const customBlocks = [
@@ -42,6 +43,7 @@ const customBlocks = [
4243
Port,
4344
SetPythonVariable,
4445
Steps,
46+
StepContainer,
4547
AdvanceToStep
4648
];
4749

src/blocks/tokens.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg {
129129
MORE_MECHANISM_METHODS_LABEL: t('BLOCKLY.MORE_MECHANISM_METHODS_LABEL'),
130130
MORE_OPMODE_METHODS_LABEL: t('BLOCKLY.MORE_OPMODE_METHODS_LABEL'),
131131
COMMENT_DEFAULT_TEXT: t('BLOCKLY.COMMENT_DEFAULT_TEXT'),
132+
STEPS: t('BLOCKLY.STEPS'),
133+
REPEAT_UNTIL: t('BLOCKLY.REPEAT_UNTIL'),
132134
}
133135
};
134136

src/i18n/locales/en/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@
146146
"GET": "get",
147147
"SET": "set",
148148
"TO": "to",
149+
"STEPS": "steps",
150+
"REPEAT_UNTIL": "Repeat Until",
149151
"CUSTOM_EVENTS_LABEL": "Custom Events",
150152
"CUSTOM_METHODS_LABEL": "Custom Methods",
151153
"MORE_ROBOT_METHODS_LABEL": "More Robot Methods",

src/i18n/locales/es/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@
147147
"GET": "obtener",
148148
"SET": "establecer",
149149
"TO": "a",
150+
"STEPS": "pasos",
151+
"REPEAT_UNTIL": "Repetir Hasta",
150152
"CUSTOM_EVENTS_LABEL": "Eventos Personalizados",
151153
"CUSTOM_METHODS_LABEL": "Métodos Personalizados",
152154
"MORE_ROBOT_METHODS_LABEL": "Más Métodos del Robot",

0 commit comments

Comments
 (0)