From d69cb2e731440dff42f16ef39beca4b0cb963c65 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 21 Jun 2025 17:47:12 -0400 Subject: [PATCH 1/6] Add ability to create event handler blocks --- src/blocks/mrc_call_python_function.ts | 2 +- src/blocks/mrc_class_method_def.ts | 6 +- src/blocks/mrc_event_handler.ts | 224 ++++++++++++++++++ src/blocks/mrc_get_parameter.ts | 18 +- src/blocks/setup_custom_blocks.ts | 4 +- src/editor/editor.ts | 5 + src/editor/extended_python_generator.ts | 20 ++ src/themes/styles.ts | 11 + ....ts => add_hardware_component_category.ts} | 0 src/toolbox/components_category.ts | 145 ++++++++++++ src/toolbox/toolbox_mechanism.ts | 4 +- 11 files changed, 430 insertions(+), 9 deletions(-) create mode 100644 src/blocks/mrc_event_handler.ts rename src/toolbox/{hardware_component_category.ts => add_hardware_component_category.ts} (100%) create mode 100644 src/toolbox/components_category.ts diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index f6fd2c9d..52c6dd72 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -612,7 +612,7 @@ export const pythonFromBlock = function( const functionName = callPythonFunctionBlock.mrcActualFunctionName ? callPythonFunctionBlock.mrcActualFunctionName : block.getFieldValue(FIELD_FUNCTION_NAME); - code = 'self.robot.' + componentName + '.' + functionName; + code = 'self.' + componentName + '.' + functionName; break; } default: diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index eadacb12..ae45c326 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -91,6 +91,8 @@ const CLASS_METHOD_DEF = { this.setStyle(MRC_STYLE_CLASS_BLOCKS); this.appendStatementInput('STACK').appendField(''); this.mrcParameters = []; + this.setPreviousStatement(false); + this.setNextStatement(false); }, /** * Returns the state of this block as a JSON serializable object. @@ -353,12 +355,12 @@ export const pythonFromBlock = function ( } if (block.mrcPythonMethodName == '__init__') { let class_specific = generator.getClassSpecificForInit(); - branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' + + branch = generator.INDENT + 'super.__init__(' + class_specific + ')\n' + generator.defineClassVariables() + branch; } else if (funcName == 'update'){ // Special case for update, to also call the update method of the base class - branch = generator.INDENT + 'self.update()\n' + branch; + branch = generator.INDENT + 'super.update()\n' + branch; } if (returnValue) { returnValue = generator.INDENT + 'return ' + returnValue + '\n'; diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts new file mode 100644 index 00000000..91e3422e --- /dev/null +++ b/src/blocks/mrc_event_handler.ts @@ -0,0 +1,224 @@ +/** + * @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 event handlers + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles'; +import { createFieldNonEditableText } from '../fields/FieldNonEditableText' +import { createFieldFlydown } from '../fields/field_flydown'; +import { Order } from 'blockly/python'; +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; + +export const BLOCK_NAME = 'mrc_event_handler'; + +export enum SenderType { + ROBOT = 'robot', + MECHANISM = 'mechanism', + COMPONENT = 'component' +} + +export type Parameter = { + name: string, + type?: string, +}; + +export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg; +interface EventHandlerMixin extends EventHandlerMixinType { + mrcPathOfSender: string, + mrcTypeOfSender: SenderType, + mrcParameters: Parameter[], +} +type EventHandlerMixinType = typeof EVENT_HANDLER; + + +/** Extra state for serialising call_python_* blocks. */ +export type EventHandlerExtraState = { + pathOfSender: string, + typeOfSender: SenderType, + + /** The parameters of the event handler. */ + params: Parameter[], +}; + +const EVENT_HANDLER = { + /** + * Block initialization. + */ + init: function (this: EventHandlerBlock): void { + this.appendDummyInput("TITLE") + .appendField('On') + .appendField(createFieldNonEditableText('sender'), 'SENDER') + .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); + this.appendDummyInput('PARAMS') + .appendField('with') + this.setOutput(false); + this.setStyle(MRC_STYLE_EVENT_HANDLER); + this.appendStatementInput('STACK').appendField(''); + this.mrcParameters = []; + this.setPreviousStatement(false); + this.setNextStatement(false); + }, + /** + * Returns the state of this block as a JSON serializable object. + */ + saveExtraState: function ( + this: EventHandlerBlock): EventHandlerExtraState { + const extraState: EventHandlerExtraState = { + pathOfSender: this.mrcPathOfSender, + typeOfSender: this.mrcTypeOfSender, + params: [], + }; + this.mrcParameters.forEach((param) => { + extraState.params.push({ + 'name': param.name, + 'type': param.type, + }); + }); + + return extraState; + }, + /** + * Applies the given state to this block. + */ + loadExtraState: function ( + this: EventHandlerBlock, + extraState: EventHandlerExtraState + ): void { + this.mrcParameters = []; + this.mrcPathOfSender = extraState.pathOfSender; + this.mrcTypeOfSender = extraState.typeOfSender; + + extraState.params.forEach((param) => { + this.mrcParameters.push({ + 'name': param.name, + 'type': param.type, + }); + }); + this.mrcUpdateParams(); + }, + /** + * Update the block to reflect the newly loaded extra state. + */ + mrcUpdateParams: function (this: EventHandlerBlock) { + if (this.mrcParameters.length > 0) { + let input = this.getInput('PARAMS'); + if (input) { + this.removeParameterFields(input); + this.mrcParameters.forEach((param) => { + const paramName = 'PARAM_' + param.name; + input.appendField(createFieldFlydown(param.name, false), paramName); + }); + } + }else{ + this.removeInput('PARAMS', true); + } + }, + removeParameterFields: function (input: Blockly.Input) { + const fieldsToRemove = input.fieldRow + .filter(field => field.name?.startsWith('PARAM_')) + .map(field => field.name!); + + fieldsToRemove.forEach(fieldName => { + input.removeField(fieldName); + }); + }, + +}; + + +export const setup = function () { + Blockly.Blocks[BLOCK_NAME] = EVENT_HANDLER; +}; + +export const pythonFromBlock = function ( + block: EventHandlerBlock, + generator: ExtendedPythonGenerator, +) { + const blocklyName = block.getFieldValue('SENDER') + '_' + block.getFieldValue('EVENT_NAME'); + + const funcName = generator.getProcedureName(blocklyName); + + let xfix1 = ''; + if (generator.STATEMENT_PREFIX) { + xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); + } + if (generator.STATEMENT_SUFFIX) { + xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); + } + if (xfix1) { + xfix1 = generator.prefixLines(xfix1, generator.INDENT); + } + let loopTrap = ''; + if (generator.INFINITE_LOOP_TRAP) { + loopTrap = generator.prefixLines( + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); + } + let branch = ''; + if (block.getInput('STACK')) { + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } + let xfix2 = ''; + if (branch && returnValue) { + // After executing the function body, revisit this block for the return. + xfix2 = xfix1; + } + + if (returnValue) { + returnValue = generator.INDENT + 'return ' + returnValue + '\n'; + } else if (!branch) { + branch = generator.PASS; + } + + let params = block.mrcParameters; + let paramString = "self"; + + if (params.length != 0) { + block.mrcParameters.forEach((param) => { + paramString += ', ' + param.name; + }); + } + + let code = 'def ' + + funcName + + '(' + + paramString + + '):\n'; + + code += + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue; + code = generator.scrub_(block, code); + generator.addClassMethodDefinition(funcName, code); + generator.addEventHandler( + block.getFieldValue('SENDER'), + block.getFieldValue('EVENT_NAME'), + funcName); + + return '' +} \ No newline at end of file diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index a3784ae7..2a50b327 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -27,6 +27,7 @@ import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; import {MRC_STYLE_VARIABLES} from '../themes/styles'; import {BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def'; +import {BLOCK_NAME as MRC_EVENT_HANDLER, EventHandlerBlock} from './mrc_event_handler'; import * as ChangeFramework from './utils/change_framework'; @@ -51,7 +52,7 @@ const GET_PARAMETER_BLOCK = { .appendField('parameter') .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); - this.setOutput(true, [OUTPUT_NAME, this.parameterType]); + this.setOutput(true, this.parameterType); ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged); }, setNameAndType: function(this: GetParameterBlock, name: string, type: string): void { @@ -64,9 +65,8 @@ const GET_PARAMETER_BLOCK = { const blockBlock = block as Blockly.Block; if (blockEvent.type === Blockly.Events.BLOCK_MOVE) { - const parent = ChangeFramework.getParentOfType(block, MRC_CLASS_METHOD_DEF); - - if (parent) { + let parent = blockBlock.getRootBlock(); + if( parent.type === MRC_CLASS_METHOD_DEF) { // It is a class method definition, so we see if this variable is in it. const classMethodDefBlock = parent as ClassMethodDefBlock; for (const parameter of classMethodDefBlock.mrcParameters) { @@ -77,6 +77,16 @@ const GET_PARAMETER_BLOCK = { } } } + else if (parent.type === MRC_EVENT_HANDLER) { + const classMethodDefBlock = parent as ClassMethodDefBlock; + for (const parameter of classMethodDefBlock.mrcParameters) { + if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) { + // If it is, we allow it to stay. + blockBlock.setWarningText(null); + return; + } + } + } // If we end up here it shouldn't be allowed block.unplug(true); blockBlock.setWarningText('Parameters can only go in their method\'s block.'); diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index 896c6040..753cc2d3 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -16,6 +16,7 @@ import * as OpModeDetails from './mrc_opmode_details'; import * as Event from './mrc_event'; import * as GetParameter from './mrc_get_parameter'; import * as ParameterMutator from './mrc_param_container' +import * as EventHandler from './mrc_event_handler'; const customBlocks = [ CallPythonFunction, @@ -34,7 +35,8 @@ const customBlocks = [ OpModeDetails, Event, GetParameter, - ParameterMutator + ParameterMutator, + EventHandler, ]; export const setup = function(forBlock: any) { diff --git a/src/editor/editor.ts b/src/editor/editor.ts index ba957898..a3938082 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -31,6 +31,7 @@ import * as toolboxRobot from '../toolbox/toolbox_robot'; //import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests'; import { MethodsCategory} from '../toolbox/methods_category'; import { EventsCategory} from '../toolbox/event_category'; +import { ComponentsCategory } from '../toolbox/components_category'; const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { @@ -46,6 +47,7 @@ export class Editor { private storage: commonStorage.Storage; private methodsCategory: MethodsCategory; private eventsCategory: EventsCategory; + private componentsCategory: ComponentsCategory; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; private projectPath: string = ''; @@ -61,6 +63,7 @@ export class Editor { this.storage = storage; this.methodsCategory = new MethodsCategory(blocklyWorkspace); this.eventsCategory = new EventsCategory(blocklyWorkspace); + this.componentsCategory = new ComponentsCategory(blocklyWorkspace); } private onChangeWhileLoading(event: Blockly.Events.Abstract) { @@ -124,6 +127,8 @@ export class Editor { this.currentModule = currentModule; this.methodsCategory.setCurrentModule(currentModule); this.eventsCategory.setCurrentModule(currentModule); + this.componentsCategory.setCurrentModule(currentModule); + if (currentModule) { this.modulePath = currentModule.modulePath; this.projectPath = commonStorage.makeProjectPath(currentModule.projectName); diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index e86a862c..f49f00b6 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -53,6 +53,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { private context: GeneratorContext | null = null; private classMethods: {[key: string]: string} = Object.create(null); + private events: {[key: string]: {sender: string, eventName: string}} = Object.create(null); private ports: {[key: string]: string} = Object.create(null); // Opmode details private details : OpModeDetails | null = null; @@ -89,6 +90,9 @@ export class ExtendedPythonGenerator extends PythonGenerator { variableDefinitions += this.INDENT + "self.define_hardware("; variableDefinitions += this.getListOfPorts(true); variableDefinitions += ')\n'; + if(this.events && Object.keys(this.events).length > 0){ + variableDefinitions += this.INDENT + "self.register_events()\n"; + } } return variableDefinitions; @@ -131,6 +135,12 @@ export class ExtendedPythonGenerator extends PythonGenerator { this.classMethods[methodName] = code; } + addEventHandler(sender: string, eventName: string, funcName: string): void { + this.events[funcName] = { + 'sender': sender, + 'eventName': eventName,} + } + /** * Add a Hardware Port */ @@ -162,9 +172,19 @@ export class ExtendedPythonGenerator extends PythonGenerator { const classDef = 'class ' + className + '(' + classParent + '):\n'; const classMethods = []; + + if (this.events && Object.keys(this.events).length > 0) { + let code = 'def register_events(self):\n'; + for (const eventName in this.events) { + const event = this.events[eventName]; + code += this.INDENT + 'self.' + event.sender + '.register_event("' + event.eventName + '", self.' + eventName + ')\n'; + } + classMethods.push(code); + } for (const name in this.classMethods) { classMethods.push(this.classMethods[name]) } + this.events = Object.create(null); this.classMethods = Object.create(null); this.ports = Object.create(null); code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); diff --git a/src/themes/styles.ts b/src/themes/styles.ts index 428b6775..dff4a07c 100644 --- a/src/themes/styles.ts +++ b/src/themes/styles.ts @@ -32,6 +32,8 @@ export const MRC_STYLE_MECHANISMS = 'mrc_style_mechanisms'; export const MRC_STYLE_COMPONENTS = 'mrc_style_components'; export const MRC_STYLE_EVENTS = 'mrc_style_events'; export const MRC_STYLE_PORTS = 'mrc_style_ports'; +export const MRC_STYLE_EVENT_HANDLER = 'mrc_style_event_handler'; +export const MRC_CATEGORY_STYLE_COMPONENTS = 'mrc_category_style_components'; export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { theme.setBlockStyle(MRC_STYLE_FUNCTIONS, { @@ -40,6 +42,12 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { colourTertiary: "#664984", hat: "" }); + theme.setBlockStyle(MRC_STYLE_EVENT_HANDLER, { + colourPrimary: "#805ba5", + colourSecondary: "#e6deed", + colourTertiary: "#664984", + hat: "cap" + }); theme.setBlockStyle(MRC_STYLE_EVENTS, { colourPrimary: "#805ba5", colourSecondary: "#e6deed", @@ -67,6 +75,9 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { theme.setCategoryStyle(MRC_CATEGORY_STYLE_METHODS, { colour: '#4A148C', }); + theme.setCategoryStyle(MRC_CATEGORY_STYLE_COMPONENTS, { + colour: '#4A148C', + }); theme.setBlockStyle(MRC_STYLE_COMMENTS, { colourPrimary: "#5b5ba5", diff --git a/src/toolbox/hardware_component_category.ts b/src/toolbox/add_hardware_component_category.ts similarity index 100% rename from src/toolbox/hardware_component_category.ts rename to src/toolbox/add_hardware_component_category.ts diff --git a/src/toolbox/components_category.ts b/src/toolbox/components_category.ts new file mode 100644 index 00000000..6ff88c42 --- /dev/null +++ b/src/toolbox/components_category.ts @@ -0,0 +1,145 @@ +/** + * @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. + */ + +/** + * @author alan@porpoiseful.com (Alan Smith) + */ + +import * as Blockly from 'blockly/core'; + +import * as toolboxItems from './items'; +import * as commonStorage from '../storage/common_storage'; +import { MRC_CATEGORY_STYLE_COMPONENTS } from '../themes/styles'; + +const CUSTOM_CATEGORY_COMPONENTS = 'COMPONENTS'; + +export const category = { + kind: 'category', + categorystyle: MRC_CATEGORY_STYLE_COMPONENTS, + name: 'Components', + custom: CUSTOM_CATEGORY_COMPONENTS, +}; + +export class ComponentsCategory { + private currentModule: commonStorage.Module | null = null; + + + constructor(blocklyWorkspace: Blockly.WorkspaceSvg) { + blocklyWorkspace.registerToolboxCategoryCallback(CUSTOM_CATEGORY_COMPONENTS, this.componentsFlyout.bind(this)); + } + + public setCurrentModule(currentModule: commonStorage.Module | null) { + this.currentModule = currentModule; + } + + public componentsFlyout(workspace: Blockly.WorkspaceSvg) { + const contents: toolboxItems.ContentsType[] = []; + + contents.push( + { + kind: 'block', + type: 'mrc_event_handler', + extraState: { + tooltip: '', + pathOfSender: '', + typeOfSender: 'component', + params: [], + }, + fields: { + SENDER: 'my_motor', + EVENT_NAME: 'on_stall', + }, + }, + { + kind: 'block', + type: 'mrc_event_handler', + extraState: { + tooltip: '', + pathOfSender: '', + typeOfSender: 'component', + params: [ + { + name: 'new_state', + type: 'boolean', + }, + ], + }, + fields: { + SENDER: 'my_touch_sensor', + EVENT_NAME: 'on_change', + }, + }, + // def set_speed(self, speed: float) -> None: + { + kind: 'block', + type: 'mrc_call_python_function', + extraState: { + functionKind: 'instance_component', + returnType: 'None', + args: [ + { + name: 'speed', + type: 'float', + }, + ], + tooltip: 'Set the motor to a speed between -1 and 1.', + importModule: '', + componentClassName: 'rev.SmartMotor', + componentName: 'my_motor', + }, + fields: { + COMPONENT_NAME: 'my_motor', + FUNC: 'set_speed', + }, + inputs: { + ARG0: { + block: { + type: 'math_number', + fields: { + NUM: 0.8, + }, + }, + }, + }, + }, + { + kind: 'block', + type: 'mrc_call_python_function', + extraState: { + functionKind: 'instance_component', + returnType: 'None', + args: [ + ], + tooltip: 'Stop the motor', + importModule: '', + componentClassName: 'rev.SmartMotor', + componentName: 'my_motor', + }, + fields: { + COMPONENT_NAME: 'my_motor', + FUNC: 'stop', + }, + }, + ); + + const toolboxInfo = { + contents: contents, + }; + + return toolboxInfo; + } +} \ No newline at end of file diff --git a/src/toolbox/toolbox_mechanism.ts b/src/toolbox/toolbox_mechanism.ts index 6f4487f0..ce841212 100644 --- a/src/toolbox/toolbox_mechanism.ts +++ b/src/toolbox/toolbox_mechanism.ts @@ -1,6 +1,7 @@ import * as common from './toolbox_common' -import { category as hardwareComponentCategory } from './hardware_component_category'; +import { category as hardwareComponentCategory } from './add_hardware_component_category'; import { category as eventCategory } from './event_category'; +import { category as componentsCategory } from './components_category'; export function getToolboxJSON( shownPythonToolboxCategories: Set | null) { @@ -9,6 +10,7 @@ export function getToolboxJSON( kind: 'categoryToolbox', contents: [ hardwareComponentCategory, + componentsCategory, eventCategory, { kind: 'sep' }, ...common.getToolboxItems(shownPythonToolboxCategories) From b4d0bb13200de55afd578d206c19b32e0ab1abe8 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 21 Jun 2025 17:56:16 -0400 Subject: [PATCH 2/6] Add some more sample event handlers --- src/toolbox/components_category.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/toolbox/components_category.ts b/src/toolbox/components_category.ts index 6ff88c42..9ce3b740 100644 --- a/src/toolbox/components_category.ts +++ b/src/toolbox/components_category.ts @@ -82,6 +82,34 @@ export class ComponentsCategory { SENDER: 'my_touch_sensor', EVENT_NAME: 'on_change', }, + }, + { + kind: 'block', + type: 'mrc_event_handler', + extraState: { + tooltip: '', + pathOfSender: '', + typeOfSender: 'component', + params: [], + }, + fields: { + SENDER: 'my_touch_sensor', + EVENT_NAME: 'on_pressed', + }, + }, + { + kind: 'block', + type: 'mrc_event_handler', + extraState: { + tooltip: '', + pathOfSender: '', + typeOfSender: 'component', + params: [], + }, + fields: { + SENDER: 'my_touch_sensor', + EVENT_NAME: 'on_released', + }, }, // def set_speed(self, speed: float) -> None: { From 83ca8e19056c49d72ea9aec2f2132589bb4e33b1 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 23 Jun 2025 10:39:00 -0400 Subject: [PATCH 3/6] Changed label to "When" to make it read cleaner --- src/blocks/mrc_event_handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 91e3422e..3ec8e479 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -63,7 +63,7 @@ const EVENT_HANDLER = { */ init: function (this: EventHandlerBlock): void { this.appendDummyInput("TITLE") - .appendField('On') + .appendField('When') .appendField(createFieldNonEditableText('sender'), 'SENDER') .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); this.appendDummyInput('PARAMS') From d6fe7395ab78da7d84681773b1f9f449b3314467 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 23 Jun 2025 10:44:36 -0400 Subject: [PATCH 4/6] Used copilot to follow google coding guidelines --- src/blocks/mrc_event_handler.ts | 358 ++++++++++++++++---------------- 1 file changed, 178 insertions(+), 180 deletions(-) diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 3ec8e479..b6cdc9cc 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -19,206 +19,204 @@ * @fileoverview Blocks for event handlers * @author alan@porpoiseful.com (Alan Smith) */ + import * as Blockly from 'blockly'; -import { MRC_STYLE_EVENT_HANDLER } from '../themes/styles'; -import { createFieldNonEditableText } from '../fields/FieldNonEditableText' -import { createFieldFlydown } from '../fields/field_flydown'; -import { Order } from 'blockly/python'; -import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import {Order} from 'blockly/python'; + +import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; +import {createFieldFlydown} from '../fields/field_flydown'; +import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; +import {MRC_STYLE_EVENT_HANDLER} from '../themes/styles'; export const BLOCK_NAME = 'mrc_event_handler'; export enum SenderType { - ROBOT = 'robot', - MECHANISM = 'mechanism', - COMPONENT = 'component' + ROBOT = 'robot', + MECHANISM = 'mechanism', + COMPONENT = 'component' } -export type Parameter = { - name: string, - type?: string, -}; +export interface Parameter { + name: string; + type?: string; +} export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.BlockSvg; + interface EventHandlerMixin extends EventHandlerMixinType { - mrcPathOfSender: string, - mrcTypeOfSender: SenderType, - mrcParameters: Parameter[], + mrcPathOfSender: string; + mrcTypeOfSender: SenderType; + mrcParameters: Parameter[]; } -type EventHandlerMixinType = typeof EVENT_HANDLER; - -/** Extra state for serialising call_python_* blocks. */ -export type EventHandlerExtraState = { - pathOfSender: string, - typeOfSender: SenderType, +type EventHandlerMixinType = typeof EVENT_HANDLER; - /** The parameters of the event handler. */ - params: Parameter[], -}; +/** Extra state for serialising event handler blocks. */ +export interface EventHandlerExtraState { + pathOfSender: string; + typeOfSender: SenderType; + /** The parameters of the event handler. */ + params: Parameter[]; +} const EVENT_HANDLER = { - /** - * Block initialization. - */ - init: function (this: EventHandlerBlock): void { - this.appendDummyInput("TITLE") - .appendField('When') - .appendField(createFieldNonEditableText('sender'), 'SENDER') - .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); - this.appendDummyInput('PARAMS') - .appendField('with') - this.setOutput(false); - this.setStyle(MRC_STYLE_EVENT_HANDLER); - this.appendStatementInput('STACK').appendField(''); - this.mrcParameters = []; - this.setPreviousStatement(false); - this.setNextStatement(false); - }, - /** - * Returns the state of this block as a JSON serializable object. - */ - saveExtraState: function ( - this: EventHandlerBlock): EventHandlerExtraState { - const extraState: EventHandlerExtraState = { - pathOfSender: this.mrcPathOfSender, - typeOfSender: this.mrcTypeOfSender, - params: [], - }; + /** + * Block initialization. + */ + init(this: EventHandlerBlock): void { + this.appendDummyInput('TITLE') + .appendField('When') + .appendField(createFieldNonEditableText('sender'), 'SENDER') + .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); + this.appendDummyInput('PARAMS') + .appendField('with'); + this.setOutput(false); + this.setStyle(MRC_STYLE_EVENT_HANDLER); + this.appendStatementInput('STACK').appendField(''); + this.mrcParameters = []; + this.setPreviousStatement(false); + this.setNextStatement(false); + }, + + /** + * Returns the state of this block as a JSON serializable object. + */ + saveExtraState(this: EventHandlerBlock): EventHandlerExtraState { + const extraState: EventHandlerExtraState = { + pathOfSender: this.mrcPathOfSender, + typeOfSender: this.mrcTypeOfSender, + params: [], + }; + + this.mrcParameters.forEach((param) => { + extraState.params.push({ + name: param.name, + type: param.type, + }); + }); + + return extraState; + }, + + /** + * Applies the given state to this block. + */ + loadExtraState(this: EventHandlerBlock, extraState: EventHandlerExtraState): void { + this.mrcParameters = []; + this.mrcPathOfSender = extraState.pathOfSender; + this.mrcTypeOfSender = extraState.typeOfSender; + + extraState.params.forEach((param) => { + this.mrcParameters.push({ + name: param.name, + type: param.type, + }); + }); + this.mrcUpdateParams(); + }, + + /** + * Update the block to reflect the newly loaded extra state. + */ + mrcUpdateParams(this: EventHandlerBlock): void { + if (this.mrcParameters.length > 0) { + const input = this.getInput('PARAMS'); + if (input) { + this.removeParameterFields(input); this.mrcParameters.forEach((param) => { - extraState.params.push({ - 'name': param.name, - 'type': param.type, - }); - }); - - return extraState; - }, - /** - * Applies the given state to this block. - */ - loadExtraState: function ( - this: EventHandlerBlock, - extraState: EventHandlerExtraState - ): void { - this.mrcParameters = []; - this.mrcPathOfSender = extraState.pathOfSender; - this.mrcTypeOfSender = extraState.typeOfSender; - - extraState.params.forEach((param) => { - this.mrcParameters.push({ - 'name': param.name, - 'type': param.type, - }); - }); - this.mrcUpdateParams(); - }, - /** - * Update the block to reflect the newly loaded extra state. - */ - mrcUpdateParams: function (this: EventHandlerBlock) { - if (this.mrcParameters.length > 0) { - let input = this.getInput('PARAMS'); - if (input) { - this.removeParameterFields(input); - this.mrcParameters.forEach((param) => { - const paramName = 'PARAM_' + param.name; - input.appendField(createFieldFlydown(param.name, false), paramName); - }); - } - }else{ - this.removeInput('PARAMS', true); - } - }, - removeParameterFields: function (input: Blockly.Input) { - const fieldsToRemove = input.fieldRow - .filter(field => field.name?.startsWith('PARAM_')) - .map(field => field.name!); - - fieldsToRemove.forEach(fieldName => { - input.removeField(fieldName); + const paramName = `PARAM_${param.name}`; + input.appendField(createFieldFlydown(param.name, false), paramName); }); - }, - + } + } else { + this.removeInput('PARAMS', true); + } + }, + + /** + * Removes parameter fields from the given input. + */ + removeParameterFields(input: Blockly.Input): void { + const fieldsToRemove = input.fieldRow + .filter(field => field.name?.startsWith('PARAM_')) + .map(field => field.name!); + + fieldsToRemove.forEach(fieldName => { + input.removeField(fieldName); + }); + }, }; +export function setup(): void { + Blockly.Blocks[BLOCK_NAME] = EVENT_HANDLER; +} -export const setup = function () { - Blockly.Blocks[BLOCK_NAME] = EVENT_HANDLER; -}; - -export const pythonFromBlock = function ( +export function pythonFromBlock( block: EventHandlerBlock, generator: ExtendedPythonGenerator, -) { - const blocklyName = block.getFieldValue('SENDER') + '_' + block.getFieldValue('EVENT_NAME'); - - const funcName = generator.getProcedureName(blocklyName); - - let xfix1 = ''; - if (generator.STATEMENT_PREFIX) { - xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); - } - if (generator.STATEMENT_SUFFIX) { - xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); - } - if (xfix1) { - xfix1 = generator.prefixLines(xfix1, generator.INDENT); - } - let loopTrap = ''; - if (generator.INFINITE_LOOP_TRAP) { - loopTrap = generator.prefixLines( - generator.injectId(generator.INFINITE_LOOP_TRAP, block), - generator.INDENT, - ); - } - let branch = ''; - if (block.getInput('STACK')) { - branch = generator.statementToCode(block, 'STACK'); - } - let returnValue = ''; - if (block.getInput('RETURN')) { - returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; - } - let xfix2 = ''; - if (branch && returnValue) { - // After executing the function body, revisit this block for the return. - xfix2 = xfix1; - } - - if (returnValue) { - returnValue = generator.INDENT + 'return ' + returnValue + '\n'; - } else if (!branch) { - branch = generator.PASS; - } - - let params = block.mrcParameters; - let paramString = "self"; - - if (params.length != 0) { - block.mrcParameters.forEach((param) => { - paramString += ', ' + param.name; - }); - } - - let code = 'def ' + - funcName + - '(' + - paramString + - '):\n'; - - code += - xfix1 + - loopTrap + - branch + - xfix2 + - returnValue; - code = generator.scrub_(block, code); - generator.addClassMethodDefinition(funcName, code); - generator.addEventHandler( - block.getFieldValue('SENDER'), - block.getFieldValue('EVENT_NAME'), - funcName); - - return '' +): string { + const blocklyName = `${block.getFieldValue('SENDER')}_${block.getFieldValue('EVENT_NAME')}`; + const funcName = generator.getProcedureName(blocklyName); + + let xfix1 = ''; + if (generator.STATEMENT_PREFIX) { + xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); + } + if (generator.STATEMENT_SUFFIX) { + xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); + } + if (xfix1) { + xfix1 = generator.prefixLines(xfix1, generator.INDENT); + } + + let loopTrap = ''; + if (generator.INFINITE_LOOP_TRAP) { + loopTrap = generator.prefixLines( + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); + } + + let branch = ''; + if (block.getInput('STACK')) { + branch = generator.statementToCode(block, 'STACK'); + } + + let returnValue = ''; + if (block.getInput('RETURN')) { + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } + + let xfix2 = ''; + if (branch && returnValue) { + // After executing the function body, revisit this block for the return. + xfix2 = xfix1; + } + + if (returnValue) { + returnValue = `${generator.INDENT}return ${returnValue}\n`; + } else if (!branch) { + branch = generator.PASS; + } + + const params = block.mrcParameters; + let paramString = 'self'; + + if (params.length !== 0) { + block.mrcParameters.forEach((param) => { + paramString += `, ${param.name}`; + }); + } + + let code = `def ${funcName}(${paramString}):\n`; + code += xfix1 + loopTrap + branch + xfix2 + returnValue; + code = generator.scrub_(block, code); + + generator.addClassMethodDefinition(funcName, code); + generator.addEventHandler( + block.getFieldValue('SENDER'), + block.getFieldValue('EVENT_NAME'), + funcName); + + return ''; } \ No newline at end of file From 78c98993ed395bc4105d404e7d0bfc16e0e9b24c Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 23 Jun 2025 12:42:11 -0400 Subject: [PATCH 5/6] Put in comment that it is fake, and making it closer to looking real --- src/toolbox/components_category.ts | 171 ++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 49 deletions(-) diff --git a/src/toolbox/components_category.ts b/src/toolbox/components_category.ts index 9ce3b740..b312cadc 100644 --- a/src/toolbox/components_category.ts +++ b/src/toolbox/components_category.ts @@ -19,6 +19,11 @@ * @author alan@porpoiseful.com (Alan Smith) */ +/** + * TODO: This is all fake right now, it will be generated dynamically + * based on the components that are available in the current module. + */ + import * as Blockly from 'blockly/core'; import * as toolboxItems from './items'; @@ -30,10 +35,78 @@ const CUSTOM_CATEGORY_COMPONENTS = 'COMPONENTS'; export const category = { kind: 'category', categorystyle: MRC_CATEGORY_STYLE_COMPONENTS, - name: 'Components', - custom: CUSTOM_CATEGORY_COMPONENTS, + name: 'Hardware', + contents: [ + { + kind: 'category', + name: 'my_motor', + custom: CUSTOM_CATEGORY_COMPONENTS, + componentConfig: { + name: 'my_motor', + type: 'rev.SmartMotor', + } + }, + { + kind: 'category', + name: 'my_touch_sensor', + custom: CUSTOM_CATEGORY_COMPONENTS, + componentConfig: { + name: 'my_touch_sensor', + type: 'rev.TouchSensor', + } + }, + { + kind: 'category', + name: 'New', + contents: [ + { + kind: 'label', + text: 'Components', + }, + { + kind: 'block', + type: 'mrc_component', + fields: { + NAME: 'my_motor', + TYPE: 'SmartMotor' + }, + extraState: { + importModule: 'smart_motor', + params: [{ name: 'motor_port', type: 'int' }], + hideParams: true + }, + }, + { + kind: 'block', + type: 'mrc_component', + fields: { + NAME: 'my_color_range_sensor', + TYPE: 'ColorRangeSensor' + }, + extraState: { + importModule: 'color_range_sensor', + params: [{ name: 'i2c_port', type: 'int' }], + hideParams: true + }, + }, + { + kind: 'block', + type: 'mrc_component', + fields: { + NAME: 'my_touch_sensor', + TYPE: 'RevTouchSensor' + }, + extraState: { + importModule: 'rev_touch_sensor', + params: [{ name: 'smartIO_port', type: 'int' }], + hideParams: true + }, + }, + ] + },] }; + export class ComponentsCategory { private currentModule: commonStorage.Module | null = null; @@ -83,7 +156,7 @@ export class ComponentsCategory { EVENT_NAME: 'on_change', }, }, - { + { kind: 'block', type: 'mrc_event_handler', extraState: { @@ -97,7 +170,7 @@ export class ComponentsCategory { EVENT_NAME: 'on_pressed', }, }, - { + { kind: 'block', type: 'mrc_event_handler', extraState: { @@ -112,56 +185,56 @@ export class ComponentsCategory { }, }, // def set_speed(self, speed: float) -> None: - { - kind: 'block', - type: 'mrc_call_python_function', - extraState: { - functionKind: 'instance_component', - returnType: 'None', - args: [ - { - name: 'speed', - type: 'float', - }, - ], - tooltip: 'Set the motor to a speed between -1 and 1.', - importModule: '', - componentClassName: 'rev.SmartMotor', - componentName: 'my_motor', - }, - fields: { - COMPONENT_NAME: 'my_motor', - FUNC: 'set_speed', - }, - inputs: { - ARG0: { - block: { - type: 'math_number', - fields: { - NUM: 0.8, - }, + { + kind: 'block', + type: 'mrc_call_python_function', + extraState: { + functionKind: 'instance_component', + returnType: 'None', + args: [ + { + name: 'speed', + type: 'float', + }, + ], + tooltip: 'Set the motor to a speed between -1 and 1.', + importModule: '', + componentClassName: 'rev.SmartMotor', + componentName: 'my_motor', + }, + fields: { + COMPONENT_NAME: 'my_motor', + FUNC: 'set_speed', + }, + inputs: { + ARG0: { + block: { + type: 'math_number', + fields: { + NUM: 0.8, }, }, }, }, - { - kind: 'block', - type: 'mrc_call_python_function', - extraState: { - functionKind: 'instance_component', - returnType: 'None', - args: [ - ], - tooltip: 'Stop the motor', - importModule: '', - componentClassName: 'rev.SmartMotor', - componentName: 'my_motor', - }, - fields: { - COMPONENT_NAME: 'my_motor', - FUNC: 'stop', - }, + }, + { + kind: 'block', + type: 'mrc_call_python_function', + extraState: { + functionKind: 'instance_component', + returnType: 'None', + args: [ + ], + tooltip: 'Stop the motor', + importModule: '', + componentClassName: 'rev.SmartMotor', + componentName: 'my_motor', }, + fields: { + COMPONENT_NAME: 'my_motor', + FUNC: 'stop', + }, + }, ); const toolboxInfo = { From 56c7295cf55af47581c64d350f8890af541f216e Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 23 Jun 2025 12:45:13 -0400 Subject: [PATCH 6/6] Changed all if( to if ( --- src/editor/extended_python_generator.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index f49f00b6..edb0ea6d 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -32,12 +32,12 @@ export class OpModeDetails { annotations() : string{ let code = ''; - if(this.enabled){ + if (this.enabled){ code += '@' + this.type + "\n"; - if(this.name){ + if (this.name){ code += '@name("' + this.name + '")\n'; } - if(this.group){ + if (this.group){ code += '@group("' + this.group + '")\n'; } } @@ -90,7 +90,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { variableDefinitions += this.INDENT + "self.define_hardware("; variableDefinitions += this.getListOfPorts(true); variableDefinitions += ')\n'; - if(this.events && Object.keys(this.events).length > 0){ + if (this.events && Object.keys(this.events).length > 0){ variableDefinitions += this.INDENT + "self.register_events()\n"; } } @@ -152,7 +152,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { let returnString = '' let firstPort = startWithFirst; for (const port in this.ports) { - if(!firstPort){ + if (!firstPort){ returnString += ', '; } else{ @@ -188,7 +188,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { this.classMethods = Object.create(null); this.ports = Object.create(null); code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); - if(annotations){ + if (annotations){ code = annotations + code; } this.details = null;