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 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) { 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; + }); +} 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); } 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 + } } ] } 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'; +} 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',