Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
37a9000
For rough prototype
alan412 Sep 12, 2025
5e3ae9a
Split step into a label and field
alan412 Sep 15, 2025
5a4d63d
Merge branch 'main' of github.com:wpilibsuite/systemcore-blocks-inter…
alan412 Oct 10, 2025
28d38ae
Change to allow field flydown for more than params
alan412 Oct 10, 2025
bf26084
Add advance to step blocks
alan412 Oct 10, 2025
4afcbff
use flydown
alan412 Oct 10, 2025
5a3e824
Merge remote-tracking branch 'origin/main' into pr_try_steps_block
alan412 Oct 24, 2025
200b773
First bit of having steps
alan412 Oct 25, 2025
7ebcb00
Make default starting out with one step
alan412 Oct 25, 2025
06ebe8e
Change from Advance to Jump
alan412 Oct 25, 2025
428f6df
beginning of generating code
alan412 Oct 25, 2025
6eb73b7
Limit Jump to step to be within steps
alan412 Oct 25, 2025
1208362
Move steps into opmode and only show when not already in opmode
alan412 Oct 26, 2025
c4af155
Call steps as part of loop if it is defined
alan412 Oct 26, 2025
b84f6fd
Clean up warnings
alan412 Oct 26, 2025
504948e
Ran format on new files
alan412 Oct 26, 2025
fa32b2d
Simplify python generation
alan412 Oct 26, 2025
1b55055
Simplify python generation
alan412 Oct 26, 2025
e54d696
Updates jump correctly
alan412 Oct 26, 2025
e18c259
remove warnings
alan412 Oct 26, 2025
5de62ad
fix typo
alan412 Oct 27, 2025
acd2813
add Shadow true blocks
alan412 Oct 30, 2025
34571e7
Bump version to 0.0.4
alan412 Oct 30, 2025
f0e4298
Change case of Repeat Until
alan412 Oct 30, 2025
c888d11
make sure self.steps is callable
alan412 Oct 30, 2025
a8775b9
Change to 2 space indentation
alan412 Oct 30, 2025
2c19451
change to 2 space indentation
alan412 Oct 30, 2025
e8346ca
Addressed review comments
alan412 Oct 30, 2025
2d53e0b
Merge branch 'main' of github.com:alan412/systemcore-blocks-interface…
alan412 Oct 30, 2025
b08d01f
In mrc_steps.ts:
lizlooney Oct 31, 2025
0610995
Added constants for INPUT_CONDITION_PREFIX and INPUT_STEP_PREFIX.
lizlooney Oct 31, 2025
dfd03a4
Moved code to add shadow True blocks from updateShape_ to compose.
lizlooney Oct 31, 2025
92b3ec7
Merge pull request #17 from lizlooney/pr_try_steps_block
alan412 Oct 31, 2025
f8f4f35
In mrc_jump_to_steps:
lizlooney Nov 4, 2025
7ef7583
Added STEPS and REPEAT_UNTIL strings to Hebrew translation file.
lizlooney Nov 5, 2025
291cd02
Added strings for mrc_jump_to_step blocks to i18n.
lizlooney Nov 5, 2025
42564a5
Fixed inaccurate comments.
lizlooney Nov 5, 2025
f51f3a9
Renamed createAdvanceToBlock to createJumpToStepBlock.
lizlooney Nov 5, 2025
dcf2a24
Merge pull request #18 from lizlooney/pr_try_steps_block
alan412 Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server_python_scripts/blocks_base_classes/opmode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/blocks/mrc_class_method_def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/blocks/mrc_event_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
105 changes: 105 additions & 0 deletions src/blocks/mrc_jump_to_step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @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 jump to a specific step.
* @author alan@porpoiseful.com (Alan Smith)
*/
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'

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;

const JUMP_TO_STEP_BLOCK = {
/**
* Block initialization.
*/
init: function (this: JumpToStepBlock): void {
this.appendDummyInput()
.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.');
},
/**
* 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 () {
Blockly.Blocks[BLOCK_NAME] = JUMP_TO_STEP_BLOCK;
};

export const pythonFromBlock = function (
block: JumpToStepBlock,
_generator: ExtendedPythonGenerator,
) {
let code = 'self._current_step = "' +
block.getFieldValue(FIELD_STEP_NAME) + '"\n';
code += 'return\n';

return code;
};
212 changes: 212 additions & 0 deletions src/blocks/mrc_step_container.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading