From fb75c5732de66663416452ad306a6d8ad018cd15 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 12:42:09 -0400 Subject: [PATCH 01/13] Add dist to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bd1f211..1c026b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # production /build +/dist # python stuff __pycache__/ From d165af69efb835a478c4a53055839715155f6502 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 12:42:22 -0400 Subject: [PATCH 02/13] Add private components to mechanism --- src/blocks/mrc_mechanism.ts | 2 +- src/blocks/mrc_mechanism_component_holder.ts | 102 ++++++++++++++++++- src/blocks/tokens.ts | 1 + src/editor/editor.ts | 40 ++++++++ src/i18n/locales/en/translation.json | 1 + src/i18n/locales/es/translation.json | 1 + src/i18n/locales/he/translation.json | 1 + src/toolbox/hardware_category.ts | 26 ++++- 8 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index a57fb02a..75e3241d 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -206,7 +206,7 @@ const MECHANISM = { if (foundMechanism) { const components: storageModuleContent.Component[] = []; - components.push(...editor.getComponentsFromMechanism(foundMechanism)); + components.push(...editor.getAllComponentsFromMechanism(foundMechanism)); // If the mechanism class name has changed, update this blcok. if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) { diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index d36dbb3b..ba6519ba 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -25,6 +25,7 @@ import { MRC_STYLE_MECHANISMS } from '../themes/styles'; import * as ChangeFramework from './utils/change_framework'; import { getLegalName } from './utils/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { Editor } from '../editor/editor'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; @@ -41,17 +42,20 @@ export const BLOCK_NAME = 'mrc_mechanism_component_holder'; const INPUT_MECHANISMS = 'MECHANISMS'; const INPUT_COMPONENTS = 'COMPONENTS'; +const INPUT_PRIVATE_COMPONENTS = 'PRIVATE_COMPONENTS'; const INPUT_EVENTS = 'EVENTS'; export const TOOLBOX_UPDATE_EVENT = 'toolbox-update-requested'; type MechanismComponentHolderExtraState = { hideMechanisms?: boolean; + hidePrivateComponents?: boolean; } export type MechanismComponentHolderBlock = Blockly.Block & MechanismComponentHolderMixin; interface MechanismComponentHolderMixin extends MechanismComponentHolderMixinType { mrcHideMechanisms: boolean; + mrcHidePrivateComponents: boolean; } type MechanismComponentHolderMixinType = typeof MECHANISM_COMPONENT_HOLDER; @@ -78,9 +82,9 @@ const MECHANISM_COMPONENT_HOLDER = { this.setInputsInline(false); this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); this.appendStatementInput(INPUT_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); + this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); this.appendStatementInput(INPUT_EVENTS).setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); - this.setOutput(false); this.setStyle(MRC_STYLE_MECHANISMS); ChangeFramework.registerCallback(MRC_COMPONENT_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); @@ -95,6 +99,9 @@ const MECHANISM_COMPONENT_HOLDER = { if (this.mrcHideMechanisms == true) { extraState.hideMechanisms = this.mrcHideMechanisms; } + if (this.mrcHidePrivateComponents == true) { + extraState.hidePrivateComponents = this.mrcHidePrivateComponents; + } return extraState; }, /** @@ -102,12 +109,14 @@ const MECHANISM_COMPONENT_HOLDER = { */ loadExtraState: function (this: MechanismComponentHolderBlock, extraState: MechanismComponentHolderExtraState): void { this.mrcHideMechanisms = (extraState.hideMechanisms == undefined) ? false : extraState.hideMechanisms; + this.mrcHidePrivateComponents = (extraState.hidePrivateComponents == undefined) ? false : extraState.hidePrivateComponents; this.updateBlock_(); }, /** * Update the block to reflect the newly loaded extra state. */ updateBlock_: function (this: MechanismComponentHolderBlock): void { + // Handle mechanisms input visibility if (this.mrcHideMechanisms) { if (this.getInput(INPUT_MECHANISMS)) { this.removeInput(INPUT_MECHANISMS) @@ -115,10 +124,23 @@ const MECHANISM_COMPONENT_HOLDER = { } else { if (this.getInput(INPUT_MECHANISMS) == null) { - this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); + this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); this.moveInputBefore(INPUT_MECHANISMS, INPUT_COMPONENTS) } } + + // Handle private components input visibility + if (this.mrcHidePrivateComponents) { + if (this.getInput(INPUT_PRIVATE_COMPONENTS)) { + this.removeInput(INPUT_PRIVATE_COMPONENTS) + } + } + else { + if (this.getInput(INPUT_PRIVATE_COMPONENTS) == null) { + this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); + this.moveInputBefore(INPUT_PRIVATE_COMPONENTS, INPUT_EVENTS) + } + } }, onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { @@ -179,6 +201,28 @@ const MECHANISM_COMPONENT_HOLDER = { return components; }, + getPrivateComponents: function (this: MechanismComponentHolderBlock): storageModuleContent.Component[] { + const components: storageModuleContent.Component[] = [] + + // Get component blocks from the PRIVATE_COMPONENTS input + const privateComponentsInput = this.getInput(INPUT_PRIVATE_COMPONENTS); + if (privateComponentsInput && privateComponentsInput.connection) { + // Walk through all connected component blocks. + let componentBlock = privateComponentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME) { + const component = (componentBlock as ComponentBlock).getComponent(); + if (component) { + components.push(component); + } + } + // Move to the next block in the chain + componentBlock = componentBlock.getNextBlock(); + } + } + + return components; + }, getEvents: function (this: MechanismComponentHolderBlock): storageModuleContent.Event[] { const events: storageModuleContent.Event[] = [] @@ -243,9 +287,11 @@ function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, genera code += '):\n'; const components = generator.statementToCode(block, INPUT_COMPONENTS); + const privateComponents = generator.statementToCode(block, INPUT_PRIVATE_COMPONENTS); - if (components) { - code += components; + const body = components + privateComponents; + if (body) { + code += body; generator.addClassMethodDefinition('define_hardware', code); } } @@ -272,6 +318,7 @@ export const pythonFromBlock = function ( */ export function hasAnyComponents(workspace: Blockly.Workspace): boolean { for (const block of workspace.getBlocksByType(BLOCK_NAME)) { + // Check regular components const componentsInput = block.getInput(INPUT_COMPONENTS); if (componentsInput && componentsInput.connection) { // Walk through all connected component blocks. @@ -284,6 +331,20 @@ export function hasAnyComponents(workspace: Blockly.Workspace): boolean { componentBlock = componentBlock.getNextBlock(); } } + + // Check private components + const privateComponentsInput = block.getInput(INPUT_PRIVATE_COMPONENTS); + if (privateComponentsInput && privateComponentsInput.connection) { + // Walk through all connected private component blocks. + let componentBlock = privateComponentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME && componentBlock.isEnabled()) { + return true; + } + // Move to the next block in the chain + componentBlock = componentBlock.getNextBlock(); + } + } } return false; } @@ -305,6 +366,20 @@ export function getComponentPorts(workspace: Blockly.Workspace, ports: {[key: st componentBlock = componentBlock.getNextBlock(); } } + + // Also include private components for port collection + const privateComponentsInput = block.getInput(INPUT_PRIVATE_COMPONENTS); + if (privateComponentsInput && privateComponentsInput.connection) { + // Walk through all connected private component blocks. + let componentBlock = privateComponentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME && componentBlock.isEnabled()) { + (componentBlock as ComponentBlock).getComponentPorts(ports); + } + // Move to the next block in the chain + componentBlock = componentBlock.getNextBlock(); + } + } }); } @@ -330,6 +405,25 @@ export function getComponents( }); } +export function getPrivateComponents( + workspace: Blockly.Workspace, + components: storageModuleContent.Component[]): void { + // Get the holder block and ask it for the private components. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const privateComponentsFromHolder: storageModuleContent.Component[] = + (block as MechanismComponentHolderBlock).getPrivateComponents(); + components.push(...privateComponentsFromHolder); + }); +} + +export function getAllComponents( + workspace: Blockly.Workspace, + components: storageModuleContent.Component[]): void { + // Get both regular and private components for when creating a mechanism + getComponents(workspace, components); + getPrivateComponents(workspace, components); +} + export function getEvents( workspace: Blockly.Workspace, events: storageModuleContent.Event[]): void { diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 45946aac..999a4a1d 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -38,6 +38,7 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { MECHANISMS: t('MECHANISMS'), OPMODES: t('OPMODES'), COMPONENTS: t('BLOCKLY.COMPONENTS'), + PRIVATE_COMPONENTS: t('BLOCKLY.PRIVATE_COMPONENTS'), EVENTS: t('BLOCKLY.EVENTS'), EVALUATE_BUT_IGNORE_RESULT: t('BLOCKLY.EVALUATE_BUT_IGNORE_RESULT'), EVALUATE_BUT_IGNORE_RESULT_TOOLTIP: diff --git a/src/editor/editor.ts b/src/editor/editor.ts index a8b6fc0a..cf8219c8 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -273,6 +273,15 @@ export class Editor { return components; } + public getAllComponentsFromWorkspace(): storageModuleContent.Component[] { + const components: storageModuleContent.Component[] = []; + if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || + this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + mechanismComponentHolder.getAllComponents(this.blocklyWorkspace, components); + } + return components; + } + public getMethodsForWithinFromWorkspace(): storageModuleContent.Method[] { const methods: storageModuleContent.Method[] = []; classMethodDef.getMethodsForWithin(this.blocklyWorkspace, methods); @@ -415,6 +424,37 @@ export class Editor { throw new Error('getComponentsFromMechanism: mechanism not found: ' + mechanism.className); } + /** + * Returns ALL components (including private components) defined in the given mechanism. + * This is used when creating mechanism blocks that need all components for port parameters. + */ + public getAllComponentsFromMechanism(mechanism: storageModule.Mechanism): storageModuleContent.Component[] { + if (this.currentModule?.modulePath === mechanism.modulePath) { + return this.getAllComponentsFromWorkspace(); + } + if (mechanism.className in this.mechanismClassNameToModuleContent) { + // For saved mechanisms, we need to reconstruct all components from the blocks + // since only public components are saved in the module content + const moduleContent = this.mechanismClassNameToModuleContent[mechanism.className]; + const blocks = moduleContent.getBlocks(); + + // Create a temporary workspace to load the mechanism's blocks + const tempWorkspace = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(blocks, tempWorkspace); + + // Extract all components (public and private) from the temporary workspace + const allComponents: storageModuleContent.Component[] = []; + mechanismComponentHolder.getAllComponents(tempWorkspace, allComponents); + + return allComponents; + } finally { + tempWorkspace.dispose(); + } + } + throw new Error('getAllComponentsFromMechanism: mechanism not found: ' + mechanism.className); + } + /** * Returns the events defined in the given mechanism. */ diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index feaffcd1..96e79058 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -46,6 +46,7 @@ "PARAMETER": "parameter", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", "COMPONENTS": "Components", + "PRIVATE_COMPONENTS": "Private Components", "EVENTS": "Events", "EVALUATE_BUT_IGNORE_RESULT": "evaluate but ignore result", "NONE": "None", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index c2dbfd82..10c13447 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -47,6 +47,7 @@ "PARAMETER": "parámetro", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", "COMPONENTS": "Componentes", + "PRIVATE_COMPONENTS": "Componentes Privados", "EVENTS": "Eventos", "EVALUATE_BUT_IGNORE_RESULT": "evaluar pero ignorar resultado", "NONE": "None", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 6b5f87b9..1bbc07c9 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -46,6 +46,7 @@ "PARAMETER": "פרמטר", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים להיכנס רק בבלוק השיטה שלהם", "COMPONENTS": "רכיבים", + "PRIVATE_COMPONENTS": "רכיבים פרטיים", "EVENTS": "אירועים", "EVALUATE_BUT_IGNORE_RESULT": "הערך אך התעלם מהתוצאה", "NONE": "אַף לֹא אֶחָד", diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index 3617e8cf..c2cf87d5 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -77,7 +77,7 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { if (mechanisms.length) { const mechanismBlocks: toolboxItems.Block[] = []; mechanisms.forEach(mechanism => { - const components = editor.getComponentsFromMechanism(mechanism); + const components = editor.getAllComponentsFromMechanism(mechanism); mechanismBlocks.push(createMechanismBlock(mechanism, components)); }); @@ -109,6 +109,24 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { contents: mechanismMethodBlocks, }); + // Get the public components from the mechanism and add the blocks for calling the component functions. + const componentsFromMechanism = editor.getComponentsFromMechanism(mechanism); + if (componentsFromMechanism.length > 0) { + const componentBlocks: toolboxItems.ContentsType[] = []; + componentsFromMechanism.forEach(component => { + componentBlocks.push({ + kind: 'category', + name: component.name, + contents: getInstanceComponentBlocks(component), + }); + }); + mechanismCategories.push({ + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_COMPONENTS'], + contents: componentBlocks, + }); + } + mechanismCategories.push(getMechanismEventHandlersCategory(editor, mechanismInRobot)); contents.push({ @@ -184,7 +202,11 @@ function getComponentsCategory( }); // Get components from the current workspace. - editor.getComponentsFromWorkspace().forEach(component => { + const componentsToShow = moduleType === storageModule.ModuleType.MECHANISM + ? editor.getAllComponentsFromWorkspace() // Show all components (including private) when editing mechanisms + : editor.getComponentsFromWorkspace(); // Show only regular components when editing robots + + componentsToShow.forEach(component => { // Get the blocks for this specific component contents.push({ kind: 'category', From 08717785329dce6fb5239a3a4a2ba0ee25b2a815 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 12:49:05 -0400 Subject: [PATCH 03/13] Preface components inside mechanism --- src/blocks/mrc_call_python_function.ts | 54 ++++++++++++++++++++++++++ src/toolbox/hardware_category.ts | 3 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 2d51f138..72236237 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -1142,6 +1142,34 @@ export function getInstanceComponentBlocks( return contents; } +export function getInstanceMechanismComponentBlocks( + component: storageModuleContent.Component, mechanismInRobot: storageModuleContent.MechanismInRobot): toolboxItems.ContentsType[] { + const contents: toolboxItems.ContentsType[] = []; + + const classData = getClassData(component.className); + if (!classData) { + throw new Error('Could not find classData for ' + component.className); + } + const functions = classData.instanceMethods; + + const componentClassData = getClassData('component.Component'); + if (!componentClassData) { + throw new Error('Could not find classData for component.Component'); + } + const componentFunctions = componentClassData.instanceMethods; + + for (const functionData of functions) { + // Skip the functions that are also defined in componentFunctions. + if (findSuperFunctionData(functionData, componentFunctions)) { + continue; + } + const block = createInstanceMechanismComponentBlock(component, functionData, mechanismInRobot); + contents.push(block); + } + + return contents; +} + function createInstanceComponentBlock( component: storageModuleContent.Component, functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { @@ -1166,6 +1194,32 @@ function createInstanceComponentBlock( return createBlock(extraState, fields, inputs); } +function createInstanceMechanismComponentBlock( + component: storageModuleContent.Component, + functionData: FunctionData, + mechanismInRobot: storageModuleContent.MechanismInRobot): toolboxItems.Block { + const extraState: CallPythonFunctionExtraState = { + functionKind: FunctionKind.INSTANCE_COMPONENT, + returnType: functionData.returnType, + args: [], + tooltip: functionData.tooltip, + importModule: '', + componentClassName: component.className, + componentName: mechanismInRobot.name + '.' + component.name, // Prefix with mechanism name + componentId: component.componentId, + }; + const fields: {[key: string]: any} = {}; + fields[FIELD_COMPONENT_NAME] = mechanismInRobot.name + '.' + component.name; // Prefix with mechanism name + fields[FIELD_FUNCTION_NAME] = functionData.functionName; + const inputs: {[key: string]: any} = {}; + // For INSTANCE_COMPONENT functions, the 0 argument is 'self', but + // self is represented by the FIELD_COMPONENT_NAME field. + // We don't include the arg for the self argument because we don't need a socket for it. + const argsWithoutSelf = functionData.args.slice(1); + processArgs(argsWithoutSelf, extraState, inputs); + return createBlock(extraState, fields, inputs); +} + export function addInstanceRobotBlocks( methods: storageModuleContent.Method[], contents: toolboxItems.ContentsType[]) { diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index c2cf87d5..f3bcbe6c 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -27,6 +27,7 @@ import { createMechanismBlock } from '../blocks/mrc_mechanism'; import { getAllPossibleComponents } from '../blocks/mrc_component'; import { getInstanceComponentBlocks, + getInstanceMechanismComponentBlocks, addInstanceRobotBlocks, addInstanceMechanismBlocks } from '../blocks/mrc_call_python_function'; import { Editor } from '../editor/editor'; @@ -117,7 +118,7 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { componentBlocks.push({ kind: 'category', name: component.name, - contents: getInstanceComponentBlocks(component), + contents: getInstanceMechanismComponentBlocks(component, mechanismInRobot), }); }); mechanismCategories.push({ From 0d3334a6e2466c810cd466c4be4c84605cbdbba8 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 13:01:50 -0400 Subject: [PATCH 04/13] Add tooltips --- src/blocks/mrc_mechanism_component_holder.ts | 35 ++++++++++++++++++-- src/blocks/tokens.ts | 2 ++ src/i18n/locales/en/translation.json | 4 ++- src/i18n/locales/es/translation.json | 4 ++- src/i18n/locales/he/translation.json | 4 ++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index ba6519ba..c25f4ed8 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -82,8 +82,16 @@ const MECHANISM_COMPONENT_HOLDER = { this.setInputsInline(false); this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); this.appendStatementInput(INPUT_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); - this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); + const privateComponentsInput = this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); + // Set tooltip on the private components field + const privateComponentsField = privateComponentsInput.fieldRow[0]; + if (privateComponentsField) { + privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP); + } this.appendStatementInput(INPUT_EVENTS).setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); + + // Update components tooltip based on private components visibility + this.updateComponentsTooltip_(); this.setOutput(false); this.setStyle(MRC_STYLE_MECHANISMS); @@ -112,6 +120,21 @@ const MECHANISM_COMPONENT_HOLDER = { this.mrcHidePrivateComponents = (extraState.hidePrivateComponents == undefined) ? false : extraState.hidePrivateComponents; this.updateBlock_(); }, + /** + * Update the components tooltip based on private components visibility. + */ + updateComponentsTooltip_: function (this: MechanismComponentHolderBlock): void { + const componentsInput = this.getInput(INPUT_COMPONENTS); + if (componentsInput && componentsInput.fieldRow[0]) { + const componentsField = componentsInput.fieldRow[0]; + // Only show tooltip if private components are also visible (not hidden) + if (!this.mrcHidePrivateComponents) { + componentsField.setTooltip(Blockly.Msg.COMPONENTS_TOOLTIP); + } else { + componentsField.setTooltip(''); + } + } + }, /** * Update the block to reflect the newly loaded extra state. */ @@ -137,10 +160,18 @@ const MECHANISM_COMPONENT_HOLDER = { } else { if (this.getInput(INPUT_PRIVATE_COMPONENTS) == null) { - this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); + const privateComponentsInput = this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); + // Set tooltip on the field + const privateComponentsField = privateComponentsInput.fieldRow[0]; + if (privateComponentsField) { + privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP); + } this.moveInputBefore(INPUT_PRIVATE_COMPONENTS, INPUT_EVENTS) } } + + // Update components tooltip based on private components visibility + this.updateComponentsTooltip_(); }, onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 999a4a1d..19076525 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -38,7 +38,9 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { MECHANISMS: t('MECHANISMS'), OPMODES: t('OPMODES'), COMPONENTS: t('BLOCKLY.COMPONENTS'), + COMPONENTS_TOOLTIP: t('BLOCKLY.TOOLTIP.COMPONENTS'), PRIVATE_COMPONENTS: t('BLOCKLY.PRIVATE_COMPONENTS'), + PRIVATE_COMPONENTS_TOOLTIP: t('BLOCKLY.TOOLTIP.PRIVATE_COMPONENTS'), EVENTS: t('BLOCKLY.EVENTS'), EVALUATE_BUT_IGNORE_RESULT: t('BLOCKLY.EVALUATE_BUT_IGNORE_RESULT'), EVALUATE_BUT_IGNORE_RESULT_TOOLTIP: diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 96e79058..b6738f5a 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -62,7 +62,9 @@ "OPMODE_TYPE": "What sort of OpMode this is", "OPMODE_ENABLED": "Whether the OpMode is shown on Driver Station", "OPMODE_NAME": "The name shown on the Driver Station. If blank will use the class name.", - "OPMODE_GROUP": "An optional group to group OpModes on Driver Station" + "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." }, "CATEGORY":{ "LISTS": "Lists", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 10c13447..327ff0f5 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -63,7 +63,9 @@ "OPMODE_TYPE": "Qué tipo de OpMode es este", "OPMODE_ENABLED": "Si el OpMode se muestra en la Estación del Conductor", "OPMODE_NAME": "El nombre mostrado en la Estación del Conductor. Si está en blanco usará el nombre de la clase.", - "OPMODE_GROUP": "Un grupo opcional para agrupar OpModes en la Estación del Conductor" + "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." }, "CATEGORY": { "LISTS": "Listas", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 1bbc07c9..6f01d44b 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -62,7 +62,9 @@ "OPMODE_TYPE": "איזה סוג של מצב פעולה זה", "OPMODE_ENABLED": "האם מצב הפעולה מוצג בתחנת הנהג", "OPMODE_NAME": "השם המוצג בתחנת הנהג. אם ריק ישתמש בשם הכיתה.", - "OPMODE_GROUP": "קבוצה אופציונלית לקיבוץ מצבי פעולה בתחנת הנהג" + "OPMODE_GROUP": "קבוצה אופציונלית לקיבוץ מצבי פעולה בתחנת הנהג", + "COMPONENTS": "רכיבים אלה גלויים במנגנון זה, ברובוט ובכל מצבי הפעולה.", + "PRIVATE_COMPONENTS": "רכיבים אלה לא יהיו גלויים ברובוט או במצבי פעולה. הם נגישים רק בתוך המנגנון הזה." }, "CATEGORY":{ "LISTS": "רשימות", From 945af6102329cda0e6a680884e99a2161f0429fd Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 13:04:11 -0400 Subject: [PATCH 05/13] Fix false warning for calling methods on components in mechanisms --- src/blocks/mrc_call_python_function.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 72236237..13f8a61b 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -547,6 +547,25 @@ const CALL_PYTHON_FUNCTION = { let foundComponent = false; const componentsInScope: storageModuleContent.Component[] = []; componentsInScope.push(...this.getComponentsFromRobot()); + + // If we're in a robot context, also include components from mechanisms + if (editor.getCurrentModuleType() === storageModule.ModuleType.ROBOT) { + editor.getMechanismsFromRobot().forEach(mechanismInRobot => { + const mechanism = editor.getMechanism(mechanismInRobot); + if (mechanism) { + const mechanismComponents = editor.getComponentsFromMechanism(mechanism); + mechanismComponents.forEach(component => { + // Create a copy of the component with the mechanism-prefixed name + const prefixedComponent = { + ...component, + name: mechanismInRobot.name + '.' + component.name + }; + componentsInScope.push(prefixedComponent); + }); + } + }); + } + if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) { componentsInScope.push(...editor.getComponentsFromWorkspace()); } From 6e4eb387ac59d8b76effc8d5118c075d6c6fee8b Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Fri, 5 Sep 2025 13:10:21 -0400 Subject: [PATCH 06/13] removed unused import --- src/blocks/mrc_mechanism_component_holder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index c25f4ed8..7052854e 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -25,7 +25,6 @@ import { MRC_STYLE_MECHANISMS } from '../themes/styles'; import * as ChangeFramework from './utils/change_framework'; import { getLegalName } from './utils/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { Editor } from '../editor/editor'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; From 8c1f26d00f3460711a52e49ca5ff29095a7298aa Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 20:41:30 -0700 Subject: [PATCH 07/13] Updated mechanism and robot module start json. --- src/modules/mechanism_start.json | 4 ++-- src/modules/robot_start.json | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/mechanism_start.json b/src/modules/mechanism_start.json index db5d0108..a20a8ef1 100644 --- a/src/modules/mechanism_start.json +++ b/src/modules/mechanism_start.json @@ -5,7 +5,7 @@ { "type": "mrc_class_method_def", "x": 10, - "y": 110, + "y": 150, "deletable": false, "editable": false, "extraState": { @@ -23,7 +23,7 @@ { "type": "mrc_class_method_def", "x": 10, - "y": 190, + "y": 230, "deletable": false, "editable": false, "extraState": { diff --git a/src/modules/robot_start.json b/src/modules/robot_start.json index 754bf6de..6de344d3 100644 --- a/src/modules/robot_start.json +++ b/src/modules/robot_start.json @@ -26,7 +26,9 @@ "y": 10, "deletable": false, "editable": false, - "extraState": {} + "extraState": { + "hidePrivateComponents" : true + } } ] } From 9f316f282c034244c9c38d51a52973c8adee7451 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 20:47:02 -0700 Subject: [PATCH 08/13] Added comment specifying that it needs to call getAllComponentsFromMechanism so it can create the port parameters. --- src/blocks/mrc_mechanism.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 75e3241d..02036c84 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -205,8 +205,9 @@ const MECHANISM = { } if (foundMechanism) { - const components: storageModuleContent.Component[] = []; - components.push(...editor.getAllComponentsFromMechanism(foundMechanism)); + // Here we need all the components (regular and private) from the mechanism because we need + // to create port parameters for all the components. + const components = editor.getAllComponentsFromMechanism(foundMechanism); // If the mechanism class name has changed, update this blcok. if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) { From 313c0a1fd98bc05ae050bc05710f4fa0edcf7b48 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 20:49:00 -0700 Subject: [PATCH 09/13] Updated mrc_mechanism_component_holder.ts Since the mrc_mechanism_component_holder block can't change once it has been placed on the workspace, the code can be simplified. --- src/blocks/mrc_mechanism_component_holder.ts | 109 ++++++++----------- 1 file changed, 44 insertions(+), 65 deletions(-) diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 7052854e..8bef1169 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -75,23 +75,10 @@ function setName(block: Blockly.BlockSvg){ const MECHANISM_COMPONENT_HOLDER = { /** - * Block initialization. - */ + * Block initialization. + */ init: function (this: MechanismComponentHolderBlock): void { this.setInputsInline(false); - this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); - this.appendStatementInput(INPUT_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); - const privateComponentsInput = this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); - // Set tooltip on the private components field - const privateComponentsField = privateComponentsInput.fieldRow[0]; - if (privateComponentsField) { - privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP); - } - this.appendStatementInput(INPUT_EVENTS).setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); - - // Update components tooltip based on private components visibility - this.updateComponentsTooltip_(); - this.setOutput(false); this.setStyle(MRC_STYLE_MECHANISMS); ChangeFramework.registerCallback(MRC_COMPONENT_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); @@ -112,65 +99,43 @@ const MECHANISM_COMPONENT_HOLDER = { return extraState; }, /** - * Applies the given state to this block. - */ + * Applies the given state to this block. + */ loadExtraState: function (this: MechanismComponentHolderBlock, extraState: MechanismComponentHolderExtraState): void { this.mrcHideMechanisms = (extraState.hideMechanisms == undefined) ? false : extraState.hideMechanisms; this.mrcHidePrivateComponents = (extraState.hidePrivateComponents == undefined) ? false : extraState.hidePrivateComponents; this.updateBlock_(); }, /** - * Update the components tooltip based on private components visibility. + * Update the block to reflect the newly loaded extra state. */ - updateComponentsTooltip_: function (this: MechanismComponentHolderBlock): void { - const componentsInput = this.getInput(INPUT_COMPONENTS); - if (componentsInput && componentsInput.fieldRow[0]) { - const componentsField = componentsInput.fieldRow[0]; - // Only show tooltip if private components are also visible (not hidden) - if (!this.mrcHidePrivateComponents) { - componentsField.setTooltip(Blockly.Msg.COMPONENTS_TOOLTIP); - } else { - componentsField.setTooltip(''); - } - } - }, - /** - * Update the block to reflect the newly loaded extra state. - */ updateBlock_: function (this: MechanismComponentHolderBlock): void { // Handle mechanisms input visibility - if (this.mrcHideMechanisms) { - if (this.getInput(INPUT_MECHANISMS)) { - this.removeInput(INPUT_MECHANISMS) - } - } - else { - if (this.getInput(INPUT_MECHANISMS) == null) { - this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); - this.moveInputBefore(INPUT_MECHANISMS, INPUT_COMPONENTS) - } + if (!this.mrcHideMechanisms) { + this.appendStatementInput(INPUT_MECHANISMS) + .setCheck(MECHANISM_OUTPUT) + .appendField(Blockly.Msg.MECHANISMS); } + const componentsField = new Blockly.FieldLabel(Blockly.Msg.COMPONENTS); + this.appendStatementInput(INPUT_COMPONENTS) + .setCheck(COMPONENT_OUTPUT) + .appendField(componentsField); + // Handle private components input visibility - if (this.mrcHidePrivateComponents) { - if (this.getInput(INPUT_PRIVATE_COMPONENTS)) { - this.removeInput(INPUT_PRIVATE_COMPONENTS) - } - } - else { - if (this.getInput(INPUT_PRIVATE_COMPONENTS) == null) { - const privateComponentsInput = this.appendStatementInput(INPUT_PRIVATE_COMPONENTS).setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.PRIVATE_COMPONENTS); - // Set tooltip on the field - const privateComponentsField = privateComponentsInput.fieldRow[0]; - if (privateComponentsField) { - privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP); - } - this.moveInputBefore(INPUT_PRIVATE_COMPONENTS, INPUT_EVENTS) - } + if (!this.mrcHidePrivateComponents) { + const privateComponentsField = new Blockly.FieldLabel(Blockly.Msg.PRIVATE_COMPONENTS); + this.appendStatementInput(INPUT_PRIVATE_COMPONENTS) + .setCheck(COMPONENT_OUTPUT) + .appendField(privateComponentsField); + // Set tooltips on both componentsField and privateComponentsField. + componentsField.setTooltip(Blockly.Msg.COMPONENTS_TOOLTIP); + privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP); } - - // Update components tooltip based on private components visibility - this.updateComponentsTooltip_(); + + this.appendStatementInput(INPUT_EVENTS) + .setCheck(EVENT_OUTPUT) + .appendField(Blockly.Msg.EVENTS); }, onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { @@ -319,9 +284,9 @@ function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, genera const components = generator.statementToCode(block, INPUT_COMPONENTS); const privateComponents = generator.statementToCode(block, INPUT_PRIVATE_COMPONENTS); - const body = components + privateComponents; - if (body) { - code += body; + const allComponents = components + privateComponents; + if (allComponents) { + code += allComponents; generator.addClassMethodDefinition('define_hardware', code); } } @@ -342,7 +307,7 @@ export const pythonFromBlock = function ( // Misc -/**n +/** * Returns true if the given workspace has a mrc_mechanism_component_holder * block that contains at least one component. */ @@ -464,3 +429,17 @@ export function getEvents( events.push(...eventsFromHolder); }); } + +/** + * Hide private components. + * This function should only be called when upgrading old projects. + */ +export function hidePrivateComponents(workspace: Blockly.Workspace) { + // Make sure the workspace is headless. + if (workspace.rendered) { + throw new Error('hidePrivateComponents should never be called with a rendered workspace.'); + } + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + (block as MechanismComponentHolderBlock).mrcHidePrivateComponents = true; + }); +} From f0f0bf798ee3c44e0eae40ced4b2954ee98c8428 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 20:51:41 -0700 Subject: [PATCH 10/13] Updated mrc_call_python_function.ts to handle mechanism component methods. The mechanismId (and mrcMechanismId) is set to the id of the mechanism so if the mechanism is renamed (in the robot), we update it. Added mrcComponentNames and mrcMapComponentNameToId. These are filled in during mrcOnLoad and are used to populate the component name dropdown field. --- src/blocks/mrc_call_python_function.ts | 241 +++++++++++++++---------- 1 file changed, 144 insertions(+), 97 deletions(-) diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 13f8a61b..441432e4 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -56,6 +56,7 @@ enum FunctionKind { const RETURN_TYPE_NONE = 'None'; +const INPUT_TITLE = 'TITLE'; const FIELD_MODULE_OR_CLASS_NAME = 'MODULE_OR_CLASS'; const FIELD_FUNCTION_NAME = 'FUNC'; const FIELD_EVENT_NAME = 'EVENT'; @@ -77,13 +78,15 @@ interface CallPythonFunctionMixin extends CallPythonFunctionMixinType { mrcTooltip: string, mrcImportModule: string, mrcActualFunctionName: string, - mrcMethodId?: string, - mrcComponentId?: string, + mrcMethodId: string, + mrcComponentId: string, mrcEventId: string, mrcMechanismId: string, mrcComponentClassName: string, mrcOriginalComponentName: string, mrcMechanismClassName: string, + mrcComponentNames: string[], + mrcMapComponentNameToId: {[componentName: string]: string}, } type CallPythonFunctionMixinType = typeof CALL_PYTHON_FUNCTION; @@ -135,7 +138,8 @@ type CallPythonFunctionExtraState = { eventId?: string, /** * The mrcMechanismId of the mrc_mechanism block that adds the mechanism to the robot. - * Specified only if the function kind is INSTANCE_MECHANISM. + * Specified only if the function kind is INSTANCE_MECHANISM, or INSTANCE_COMPONENT if the + * component belongs to a mechanism. */ mechanismId?: string, /** @@ -202,8 +206,14 @@ const CALL_PYTHON_FUNCTION = { case FunctionKind.INSTANCE_COMPONENT: { const className = this.mrcComponentClassName; const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); - tooltip = 'Calls the instance method ' + className + '.' + functionName + - ' on the component named ' + this.getFieldValue(FIELD_COMPONENT_NAME) + '.'; + if (this.mrcMechanismId) { + tooltip = 'Calls the instance method ' + className + '.' + functionName + + ' on the component named ' + this.getFieldValue(FIELD_COMPONENT_NAME) + + ' in the mechanism named ' + this.getFieldValue(FIELD_MECHANISM_NAME) + '.'; + } else { + tooltip = 'Calls the instance method ' + className + '.' + functionName + + ' on the component named ' + this.getFieldValue(FIELD_COMPONENT_NAME) + '.'; + } break; } case FunctionKind.INSTANCE_ROBOT: { @@ -260,6 +270,14 @@ const CALL_PYTHON_FUNCTION = { } if (this.mrcComponentId) { extraState.componentId = this.mrcComponentId; + if (this.getField(FIELD_COMPONENT_NAME)) { + // Since the user may have chosen a different component name from the dropdown, we need to get + // the componentId of the component that the user has chosen. + const componentName = this.getFieldValue(FIELD_COMPONENT_NAME); + if (componentName in this.mrcMapComponentNameToId) { + extraState.componentId = this.mrcMapComponentNameToId[componentName]; + } + } } if (this.mrcEventId) { extraState.eventId = this.mrcEventId; @@ -270,22 +288,6 @@ const CALL_PYTHON_FUNCTION = { if (this.mrcComponentClassName) { extraState.componentClassName = this.mrcComponentClassName; } - if (this.getField(FIELD_COMPONENT_NAME)) { - extraState.componentName = this.getFieldValue(FIELD_COMPONENT_NAME); - // The component name field is a drop down where the user can choose between different - // components of the same type. For example, they can easily switch from a motor component - // name "left_motor" to a motor component named "right_motor". - if (extraState.componentName !== this.mrcOriginalComponentName) { - // The user has chosen a different component name. We need to get the componentId of the - // component that the user has chosen. - for (const component of this.getComponentsFromRobot()) { - if (component.name == extraState.componentName) { - extraState.componentId = component.componentId; - break; - } - } - } - } if (this.mrcMechanismClassName) { extraState.mechanismClassName = this.mrcMechanismClassName; } @@ -315,9 +317,10 @@ const CALL_PYTHON_FUNCTION = { this.mrcEventId = extraState.eventId ? extraState.eventId : ''; this.mrcMechanismId = extraState.mechanismId ? extraState.mechanismId : ''; this.mrcComponentClassName = extraState.componentClassName ? extraState.componentClassName : ''; - this.mrcOriginalComponentName = extraState.componentName - ? extraState.componentName : ''; this.mrcMechanismClassName = extraState.mechanismClassName ? extraState.mechanismClassName : ''; + // Initialize mrcComponentNames and mrcMapComponentNameToId here. They will be filled during mrcOnLoad. + this.mrcComponentNames = []; + this.mrcMapComponentNameToId = {}; this.updateBlock_(); }, /** @@ -341,73 +344,76 @@ const CALL_PYTHON_FUNCTION = { this.setOutput(false); } - if (!this.getInput('TITLE')) { + if (!this.getInput(INPUT_TITLE)) { // Add the dummy input. switch (this.mrcFunctionKind) { case FunctionKind.BUILT_IN: - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); break; case FunctionKind.MODULE: - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_MODULE_OR_CLASS_NAME) .appendField('.') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); break; case FunctionKind.STATIC: - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_MODULE_OR_CLASS_NAME) .appendField('.') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); break; case FunctionKind.CONSTRUCTOR: - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('create') .appendField(createFieldNonEditableText(''), FIELD_MODULE_OR_CLASS_NAME); break; case FunctionKind.INSTANCE: - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_MODULE_OR_CLASS_NAME) .appendField('.') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); break; case FunctionKind.INSTANCE_WITHIN: { - const input = this.getInput('TITLE'); + const input = this.getInput(INPUT_TITLE); if (!input) { - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); } break; } case FunctionKind.EVENT: { - const input = this.getInput('TITLE'); + const input = this.getInput(INPUT_TITLE); if (!input) { - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('fire') .appendField(createFieldNonEditableText(''), FIELD_EVENT_NAME); } break; } case FunctionKind.INSTANCE_COMPONENT: { - const componentNameChoices : string[] = []; - this.getComponentsFromRobot().forEach(component => componentNameChoices.push(component.name)); - if (!componentNameChoices.includes(this.mrcOriginalComponentName)) { - componentNameChoices.push(this.mrcOriginalComponentName); + const titleInput = this.appendDummyInput(INPUT_TITLE) + .appendField('call'); + if (this.mrcMechanismId) { + titleInput + .appendField(createFieldNonEditableText(''), FIELD_MECHANISM_NAME) + .appendField('.'); } - this.appendDummyInput('TITLE') - .appendField('call') - .appendField(createFieldDropdown(componentNameChoices), FIELD_COMPONENT_NAME) + // Here we create a text field for the component name. + // Later, in mrcOnLoad, we will replace it with a dropdown. + titleInput + .appendField(createFieldNonEditableText(''), FIELD_COMPONENT_NAME) .appendField('.') .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); break; } case FunctionKind.INSTANCE_ROBOT: { - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText('robot')) .appendField('.') @@ -415,7 +421,7 @@ const CALL_PYTHON_FUNCTION = { break; } case FunctionKind.INSTANCE_MECHANISM: { - this.appendDummyInput('TITLE') + this.appendDummyInput(INPUT_TITLE) .appendField('call') .appendField(createFieldNonEditableText(''), FIELD_MECHANISM_NAME) .appendField('.') @@ -469,6 +475,11 @@ const CALL_PYTHON_FUNCTION = { if (id === this.mrcComponentId) { this.setFieldValue(newName, FIELD_COMPONENT_NAME); } + if (this.mrcMechanismId) { + if (id === this.mrcMechanismId) { + this.setFieldValue(newName, FIELD_MECHANISM_NAME); + } + } break; case FunctionKind.INSTANCE_ROBOT: if (id === this.mrcMethodId) { @@ -517,12 +528,36 @@ const CALL_PYTHON_FUNCTION = { } this.updateBlock_(); }, - getComponentsFromRobot: function(this: CallPythonFunctionBlock): storageModuleContent.Component[] { + getComponents: function(this: CallPythonFunctionBlock): storageModuleContent.Component[] { // Get the list of components whose type matches this.mrcComponentClassName. const components: storageModuleContent.Component[] = []; const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); if (editor) { - editor.getComponentsFromRobot().forEach(component => { + let componentsToConsider: storageModuleContent.Component[] = []; + if (this.mrcMechanismId) { + // Only consider components that belong to the mechanism. + // this.mrcMechanismId is the mechanismId from the MechanismInRobot. + // We need to get the MechanismInRobot with that id, then get the mechanism, and then get + // the public components defined in that mechanism. + for (const mechanismInRobot of editor.getMechanismsFromRobot()) { + if (mechanismInRobot.mechanismId === this.mrcMechanismId) { + for (const mechanism of editor.getMechanisms()) { + if (mechanism.moduleId === mechanismInRobot.moduleId) { + componentsToConsider = editor.getComponentsFromMechanism(mechanism); + break; + } + } + break; + } + } + } else if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) { + // Only consider components (regular and private) in the current workspace. + componentsToConsider = editor.getAllComponentsFromWorkspace(); + } else { + // Only consider components in the robot. + componentsToConsider = editor.getComponentsFromRobot(); + } + componentsToConsider.forEach(component => { if (component.className === this.mrcComponentClassName) { components.push(component); } @@ -543,58 +578,43 @@ const CALL_PYTHON_FUNCTION = { // If the component doesn't exist, put a visible warning on this block. // If the component has changed, update the block if possible or put a // visible warning on it. + // If the component belongs to a mechanism, also check whether the mechanism + // still exists and whether it has been changed. if (this.mrcFunctionKind === FunctionKind.INSTANCE_COMPONENT) { + this.getComponents().forEach(component => { + this.mrcComponentNames.push(component.name); + this.mrcMapComponentNameToId[component.name] = component.componentId; + }); let foundComponent = false; - const componentsInScope: storageModuleContent.Component[] = []; - componentsInScope.push(...this.getComponentsFromRobot()); - - // If we're in a robot context, also include components from mechanisms - if (editor.getCurrentModuleType() === storageModule.ModuleType.ROBOT) { - editor.getMechanismsFromRobot().forEach(mechanismInRobot => { - const mechanism = editor.getMechanism(mechanismInRobot); - if (mechanism) { - const mechanismComponents = editor.getComponentsFromMechanism(mechanism); - mechanismComponents.forEach(component => { - // Create a copy of the component with the mechanism-prefixed name - const prefixedComponent = { - ...component, - name: mechanismInRobot.name + '.' + component.name - }; - componentsInScope.push(prefixedComponent); - }); - } - }); - } - - if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) { - componentsInScope.push(...editor.getComponentsFromWorkspace()); - } - for (const component of componentsInScope) { - if (component.componentId === this.mrcComponentId) { + for (const componentName of this.mrcComponentNames) { + const componentId = this.mrcMapComponentNameToId[componentName]; + if (componentId === this.mrcComponentId) { foundComponent = true; - // If the component name has changed, we can handle that. - if (this.getFieldValue(FIELD_COMPONENT_NAME) !== component.name) { - // Replace the FIELD_COMPONENT_NAME field. - const titleInput = this.getInput('TITLE') - if (titleInput) { - let indexOfComponentName = -1; - for (let i = 0, field; (field = titleInput.fieldRow[i]); i++) { - if (field.name === FIELD_COMPONENT_NAME) { - indexOfComponentName = i; - break; - } - } - if (indexOfComponentName != -1) { - const componentNameChoices : string[] = []; - componentsInScope.forEach(component => componentNameChoices.push(component.name)); - titleInput.removeField(FIELD_COMPONENT_NAME); - titleInput.insertFieldAt(indexOfComponentName, - createFieldDropdown(componentNameChoices), FIELD_COMPONENT_NAME); - } - this.setFieldValue(component.name, FIELD_COMPONENT_NAME); + // Replace the text field for the component name with a dropdown where the user can choose + // between different components of the same type. For example, they can easily switch from + // a motor component name "left_motor" to a motor component named "right_motor". + const titleInput = this.getInput(INPUT_TITLE) + if (!titleInput) { + throw new Error('Could not find the title input'); + } + let indexOfComponentNameField = -1; + for (let i = 0, field; (field = titleInput.fieldRow[i]); i++) { + if (field.name === FIELD_COMPONENT_NAME) { + indexOfComponentNameField = i; + break; } } + if (indexOfComponentNameField == -1) { + throw new Error('Could not find the component name field'); + } + titleInput.removeField(FIELD_COMPONENT_NAME); + titleInput.insertFieldAt(indexOfComponentNameField, + createFieldDropdown(this.mrcComponentNames), FIELD_COMPONENT_NAME); + // TODO(lizlooney): If the current module is the robot or a mechanism, we need to update the + // items in the dropdown if the user adds or removes a component. + + this.setFieldValue(componentName, FIELD_COMPONENT_NAME); // Since we found the component, we can break out of the loop. break; @@ -604,6 +624,27 @@ const CALL_PYTHON_FUNCTION = { warnings.push('This block calls a method on a component that no longer exists.'); } + if (this.mrcMechanismId) { + let foundMechanism = false; + const mechanismsInRobot = editor.getMechanismsFromRobot(); + for (const mechanismInRobot of mechanismsInRobot) { + if (mechanismInRobot.mechanismId === this.mrcMechanismId) { + foundMechanism = true; + + // If the mechanism name has changed, we can handle that. + if (this.getFieldValue(FIELD_MECHANISM_NAME) !== mechanismInRobot.name) { + this.setFieldValue(mechanismInRobot.name, FIELD_MECHANISM_NAME); + } + break; + } + } + if (!foundMechanism) { + warnings.push( + 'This block calls a method on a component that belongs to a mechanism that no ' + + 'longer exists.'); + } + } + // TODO(lizlooney): Could the component's method have change or been deleted? } @@ -812,10 +853,6 @@ export function pythonFromBlock( break; } case FunctionKind.INSTANCE_COMPONENT: { - const componentName = block.getFieldValue(FIELD_COMPONENT_NAME); - const functionName = block.mrcActualFunctionName - ? block.mrcActualFunctionName - : block.getFieldValue(FIELD_FUNCTION_NAME); // Generate the correct code depending on the module type. switch (generator.getModuleType()) { case storageModule.ModuleType.ROBOT: @@ -826,6 +863,14 @@ export function pythonFromBlock( code = 'self.robot.'; break; } + if (block.mrcMechanismId) { + const mechanismName = block.getFieldValue(FIELD_MECHANISM_NAME); + code += mechanismName + '.'; + } + const componentName = block.getFieldValue(FIELD_COMPONENT_NAME); + const functionName = block.mrcActualFunctionName + ? block.mrcActualFunctionName + : block.getFieldValue(FIELD_FUNCTION_NAME); code += componentName + '.' + functionName; break; } @@ -1214,7 +1259,7 @@ function createInstanceComponentBlock( } function createInstanceMechanismComponentBlock( - component: storageModuleContent.Component, + component: storageModuleContent.Component, functionData: FunctionData, mechanismInRobot: storageModuleContent.MechanismInRobot): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { @@ -1224,11 +1269,13 @@ function createInstanceMechanismComponentBlock( tooltip: functionData.tooltip, importModule: '', componentClassName: component.className, - componentName: mechanismInRobot.name + '.' + component.name, // Prefix with mechanism name + componentName: component.name, componentId: component.componentId, + mechanismId: mechanismInRobot.mechanismId, }; const fields: {[key: string]: any} = {}; - fields[FIELD_COMPONENT_NAME] = mechanismInRobot.name + '.' + component.name; // Prefix with mechanism name + fields[FIELD_MECHANISM_NAME] = mechanismInRobot.name; + fields[FIELD_COMPONENT_NAME] = component.name; fields[FIELD_FUNCTION_NAME] = functionData.functionName; const inputs: {[key: string]: any} = {}; // For INSTANCE_COMPONENT functions, the 0 argument is 'self', but From 21c7680a8e297d0d915f4ed816a237ad8c88c6ab Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 21:06:06 -0700 Subject: [PATCH 11/13] Set current version to 0.0.2. Added privateComponents to module content. We can use this to get the private components from a mechanism without having to use a headless blockly workspace to process the blocks. Added code to upgrade projects to version 0.0.2, which involves updating the mrc_mechanism_component_holder block in the robot and adding privateComponents in module content. --- src/storage/module_content.ts | 35 ++++++++++++++++--- src/storage/project.ts | 4 +-- src/storage/upgrade_project.ts | 61 +++++++++++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/storage/module_content.ts b/src/storage/module_content.ts index a41e4eb5..61a32eed 100644 --- a/src/storage/module_content.ts +++ b/src/storage/module_content.ts @@ -60,9 +60,10 @@ export type Event = { }; function startingBlocksToModuleContentText( - module: storageModule.Module, startingBlocks: { [key: string]: any }): string { + module: storageModule.Module, startingBlocks: {[key: string]: any}): string { const mechanisms: MechanismInRobot[] = []; const components: Component[] = []; + const privateComponents: Component[] = []; const events: Event[] = []; const methods: Method[] = []; return makeModuleContentText( @@ -70,6 +71,7 @@ function startingBlocksToModuleContentText( startingBlocks, mechanisms, components, + privateComponents, events, methods); } @@ -125,9 +127,10 @@ export function newOpModeContent(projectName: string, opModeClassName: string): */ export function makeModuleContentText( module: storageModule.Module, - blocks: { [key: string]: any }, + blocks: {[key: string]: any}, mechanisms: MechanismInRobot[], components: Component[], + privateComponents: Component[], events: Event[], methods: Method[]): string { if (!module.moduleId) { @@ -139,6 +142,7 @@ export function makeModuleContentText( blocks, mechanisms, components, + privateComponents, events, methods); return moduleContent.getModuleContentText(); @@ -151,6 +155,7 @@ export function parseModuleContentText(moduleContentText: string): ModuleContent !('blocks' in parsedContent) || !('mechanisms' in parsedContent) || !('components' in parsedContent) || + !('privateComponents' in parsedContent) || !('events' in parsedContent) || !('methods' in parsedContent)) { throw new Error('Module content text is not valid.'); @@ -161,6 +166,7 @@ export function parseModuleContentText(moduleContentText: string): ModuleContent parsedContent.blocks, parsedContent.mechanisms, parsedContent.components, + parsedContent.privateComponents, parsedContent.events, parsedContent.methods); } @@ -169,9 +175,10 @@ export class ModuleContent { constructor( private moduleType: storageModule.ModuleType, private moduleId: string, - private blocks : { [key: string]: any }, + private blocks : {[key: string]: any}, private mechanisms: MechanismInRobot[], private components: Component[], + private privateComponents: Component[], private events: Event[], private methods: Method[]) { } @@ -188,10 +195,14 @@ export class ModuleContent { return this.moduleId; } - getBlocks(): { [key: string]: any } { + getBlocks(): {[key: string]: any} { return this.blocks; } + setBlocks(blocks: {[key: string]: any}): void { + this.blocks = blocks; + } + getMechanisms(): MechanismInRobot[] { return this.mechanisms; } @@ -200,6 +211,10 @@ export class ModuleContent { return this.components; } + getPrivateComponents(): Component[] { + return this.privateComponents; + } + getEvents(): Event[] { return this.events; } @@ -252,3 +267,15 @@ export class ModuleContent { } } } + +/** + * Add privateComponents field. + * This function should only called when upgrading old projects. + */ +export function addPrivateComponents(moduleContentText: string): string { + const parsedContent = JSON.parse(moduleContentText); + if (!('privateComponents' in parsedContent)) { + parsedContent.privateComponents = []; + } + return JSON.stringify(parsedContent, null, 2); +} diff --git a/src/storage/project.ts b/src/storage/project.ts index 7292fbd1..153051f1 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -37,9 +37,9 @@ export type Project = { }; const NO_VERSION = '0.0.0'; -export const CURRENT_VERSION = '0.0.1'; +export const CURRENT_VERSION = '0.0.2'; -type ProjectInfo = { +export type ProjectInfo = { version: string, }; diff --git a/src/storage/upgrade_project.ts b/src/storage/upgrade_project.ts index 8aa6687e..4cfaae75 100644 --- a/src/storage/upgrade_project.ts +++ b/src/storage/upgrade_project.ts @@ -20,8 +20,13 @@ */ import * as semver from 'semver'; +import * as Blockly from 'blockly/core'; +import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_holder'; import * as commonStorage from './common_storage'; +import * as storageModule from './module'; +import * as storageModuleContent from './module_content'; +import * as storageNames from './names'; import * as storageProject from './project'; @@ -30,12 +35,60 @@ export async function upgradeProjectIfNecessary( const projectInfo = await storageProject.fetchProjectInfo(storage, projectName); if (semver.lt(projectInfo.version, storageProject.CURRENT_VERSION)) { switch (projectInfo.version) { + // @ts-ignore case '0.0.0': - // Project was saved without a project.info.json file. - // Nothing needs to be done to upgrade to '0.0.1'; - projectInfo.version = '0.0.1'; - break; + upgradeFrom_000_to_001(storage, projectName, projectInfo) + // Intentional fallthrough + case '0.0.1': + upgradeFrom_001_to_002(storage, projectName, projectInfo); } await storageProject.saveProjectInfo(storage, projectName); } } + +async function upgradeFrom_000_to_001( + _storage: commonStorage.Storage, + _projectName: string, + projectInfo: storageProject.ProjectInfo): Promise { + // Project was saved without a project.info.json file. + // Nothing needs to be done to upgrade to '0.0.1'; + projectInfo.version = '0.0.1'; +} + +async function upgradeFrom_001_to_002( + storage: commonStorage.Storage, + projectName: string, + projectInfo: storageProject.ProjectInfo): Promise { + // Modules were saved without private components. + // The Robot's mrc_mechanism_component_holder block was saved without hidePrivateComponents. + const projectFileNames: string[] = await storage.list( + storageNames.makeProjectDirectoryPath(projectName)); + for (const projectFileName of projectFileNames) { + const modulePath = storageNames.makeFilePath(projectName, projectFileName); + let moduleContentText = await storage.fetchFileContentText(modulePath); + + // Add private components to the module content. + moduleContentText = storageModuleContent.addPrivateComponents(moduleContentText); + + if (storageNames.getModuleType(modulePath) === storageModule.ModuleType.ROBOT) { + // If this module is the robot, hide the private components part of the + // mrc_mechanism_component_holder block. + const moduleContent = storageModuleContent.parseModuleContentText(moduleContentText); + let blocks = moduleContent.getBlocks(); + // Create a temporary workspace to upgrade the blocks. + const headlessWorkspace = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(blocks, headlessWorkspace); + mechanismComponentHolder.hidePrivateComponents(headlessWorkspace); + blocks = Blockly.serialization.workspaces.save(headlessWorkspace); + } finally { + headlessWorkspace.dispose(); + } + moduleContent.setBlocks(blocks); + moduleContentText = moduleContent.getModuleContentText(); + } + + await storage.saveFile(modulePath, moduleContentText); + } + projectInfo.version = '0.0.2'; +} From 19f1003d4c0225db6a7f42891cb14fbdfefd815c Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 21:07:19 -0700 Subject: [PATCH 12/13] Added code to editor to get the private components and pass them to storageModuleContent.makeModuleContentText. Made some methods private where possible. Modified getAllComponentsFromMechanism so it gets the private components from a mechanism's module content without having to use a headless blockly workspace to process the mechanism's blocks. --- src/editor/editor.ts | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/editor/editor.ts b/src/editor/editor.ts index cf8219c8..0b6a5351 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -246,6 +246,7 @@ export class Editor { const blocks = Blockly.serialization.workspaces.save(this.blocklyWorkspace); const mechanisms: storageModuleContent.MechanismInRobot[] = this.getMechanismsFromWorkspace(); const components: storageModuleContent.Component[] = this.getComponentsFromWorkspace(); + const privateComponents: storageModuleContent.Component[] = this.getPrivateComponentsFromWorkspace(); const events: storageModuleContent.Event[] = this.getEventsFromWorkspace(); const methods: storageModuleContent.Method[] = ( this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || @@ -253,10 +254,10 @@ export class Editor { ? this.getMethodsForOutsideFromWorkspace() : []; return storageModuleContent.makeModuleContentText( - this.currentModule, blocks, mechanisms, components, events, methods); + this.currentModule, blocks, mechanisms, components, privateComponents, events, methods); } - public getMechanismsFromWorkspace(): storageModuleContent.MechanismInRobot[] { + private getMechanismsFromWorkspace(): storageModuleContent.MechanismInRobot[] { const mechanisms: storageModuleContent.MechanismInRobot[] = []; if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { mechanismComponentHolder.getMechanisms(this.blocklyWorkspace, mechanisms); @@ -264,7 +265,7 @@ export class Editor { return mechanisms; } - public getComponentsFromWorkspace(): storageModuleContent.Component[] { + private getComponentsFromWorkspace(): storageModuleContent.Component[] { const components: storageModuleContent.Component[] = []; if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { @@ -273,6 +274,14 @@ export class Editor { return components; } + private getPrivateComponentsFromWorkspace(): storageModuleContent.Component[] { + const components: storageModuleContent.Component[] = []; + if (this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + mechanismComponentHolder.getPrivateComponents(this.blocklyWorkspace, components); + } + return components; + } + public getAllComponentsFromWorkspace(): storageModuleContent.Component[] { const components: storageModuleContent.Component[] = []; if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || @@ -288,7 +297,7 @@ export class Editor { return methods; } - public getMethodsForOutsideFromWorkspace(): storageModuleContent.Method[] { + private getMethodsForOutsideFromWorkspace(): storageModuleContent.Method[] { const methods: storageModuleContent.Method[] = []; classMethodDef.getMethodsForOutside(this.blocklyWorkspace, methods); return methods; @@ -433,24 +442,12 @@ export class Editor { return this.getAllComponentsFromWorkspace(); } if (mechanism.className in this.mechanismClassNameToModuleContent) { - // For saved mechanisms, we need to reconstruct all components from the blocks - // since only public components are saved in the module content const moduleContent = this.mechanismClassNameToModuleContent[mechanism.className]; - const blocks = moduleContent.getBlocks(); - - // Create a temporary workspace to load the mechanism's blocks - const tempWorkspace = new Blockly.Workspace(); - try { - Blockly.serialization.workspaces.load(blocks, tempWorkspace); - - // Extract all components (public and private) from the temporary workspace - const allComponents: storageModuleContent.Component[] = []; - mechanismComponentHolder.getAllComponents(tempWorkspace, allComponents); - - return allComponents; - } finally { - tempWorkspace.dispose(); - } + const allComponents: storageModuleContent.Component[] = [ + ...moduleContent.getComponents(), + ...moduleContent.getPrivateComponents(), + ] + return allComponents; } throw new Error('getAllComponentsFromMechanism: mechanism not found: ' + mechanism.className); } From d7cbbb35d40352117864835ef310ce00e808a5b4 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 8 Sep 2025 21:11:01 -0700 Subject: [PATCH 13/13] Added comment in getRobotMechanismsCategory specifying that it needs to call getAllComponentsFromMechanism so it can create the port parameters. Simplified getComponentsCategory to call getAllComponentsFromWorkspace for both robot and mechanism modules and added comments explaining why. --- src/toolbox/hardware_category.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index f3bcbe6c..8a5c3c1c 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -78,6 +78,8 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { if (mechanisms.length) { const mechanismBlocks: toolboxItems.Block[] = []; mechanisms.forEach(mechanism => { + // Here we need all the components (regular and private) from the mechanism because we need + // to create port parameters for all the components. const components = editor.getAllComponentsFromMechanism(mechanism); mechanismBlocks.push(createMechanismBlock(mechanism, components)); }); @@ -110,11 +112,13 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { contents: mechanismMethodBlocks, }); - // Get the public components from the mechanism and add the blocks for calling the component functions. + // Get the public components from the mechanism and add the blocks for calling the + // component functions. const componentsFromMechanism = editor.getComponentsFromMechanism(mechanism); if (componentsFromMechanism.length > 0) { const componentBlocks: toolboxItems.ContentsType[] = []; componentsFromMechanism.forEach(component => { + // Get the blocks for this specific component. componentBlocks.push({ kind: 'category', name: component.name, @@ -202,12 +206,13 @@ function getComponentsCategory( contents: getAllPossibleComponents(moduleType), }); - // Get components from the current workspace. - const componentsToShow = moduleType === storageModule.ModuleType.MECHANISM - ? editor.getAllComponentsFromWorkspace() // Show all components (including private) when editing mechanisms - : editor.getComponentsFromWorkspace(); // Show only regular components when editing robots - - componentsToShow.forEach(component => { + // Get all (regular and private) components from the current workspace. + // For a robot module, we can only have regular components. For a mechanism module, we can have + // regular and/or private components. Rather than checking what the current module type is, it's + // simpler to just call getAllComponentsFromWorkspace for both robot and mechanism modules. Since + // robot modules don't have private components, getAllComponentsFromWorkspace is equivalent to + // getComponentsFromWorkspace for a robot module. + editor.getAllComponentsFromWorkspace().forEach(component => { // Get the blocks for this specific component contents.push({ kind: 'category',