diff --git a/.gitignore b/.gitignore index 0bd1f211..1c026b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # production /build +/dist # python stuff __pycache__/ diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 2d51f138..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,39 +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 (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; @@ -585,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? } @@ -793,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: @@ -807,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; } @@ -1142,6 +1206,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 +1258,34 @@ 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: component.name, + componentId: component.componentId, + mechanismId: mechanismInRobot.mechanismId, + }; + const fields: {[key: string]: any} = {}; + 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 + // 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/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index a57fb02a..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.getComponentsFromMechanism(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 d36dbb3b..8bef1169 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -41,17 +41,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; @@ -72,15 +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); - 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,30 +93,49 @@ const MECHANISM_COMPONENT_HOLDER = { if (this.mrcHideMechanisms == true) { extraState.hideMechanisms = this.mrcHideMechanisms; } + if (this.mrcHidePrivateComponents == true) { + extraState.hidePrivateComponents = this.mrcHidePrivateComponents; + } 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 block to reflect the newly loaded extra state. - */ + * Update the block to reflect the newly loaded extra state. + */ updateBlock_: function (this: MechanismComponentHolderBlock): void { - if (this.mrcHideMechanisms) { - if (this.getInput(INPUT_MECHANISMS)) { - this.removeInput(INPUT_MECHANISMS) - } + // Handle mechanisms input visibility + if (!this.mrcHideMechanisms) { + this.appendStatementInput(INPUT_MECHANISMS) + .setCheck(MECHANISM_OUTPUT) + .appendField(Blockly.Msg.MECHANISMS); } - else { - if (this.getInput(INPUT_MECHANISMS) == null) { - this.appendStatementInput(INPUT_MECHANISMS).setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); - this.moveInputBefore(INPUT_MECHANISMS, INPUT_COMPONENTS) - } + + 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) { + 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); } + + 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) { @@ -179,6 +196,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 +282,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 allComponents = components + privateComponents; + if (allComponents) { + code += allComponents; generator.addClassMethodDefinition('define_hardware', code); } } @@ -266,12 +307,13 @@ 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. */ 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 +326,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 +361,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 +400,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 { @@ -340,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/blocks/tokens.ts b/src/blocks/tokens.ts index 45946aac..19076525 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -38,6 +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/editor/editor.ts b/src/editor/editor.ts index a8b6fc0a..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,13 +274,30 @@ 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 || + 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); return methods; } - public getMethodsForOutsideFromWorkspace(): storageModuleContent.Method[] { + private getMethodsForOutsideFromWorkspace(): storageModuleContent.Method[] { const methods: storageModuleContent.Method[] = []; classMethodDef.getMethodsForOutside(this.blocklyWorkspace, methods); return methods; @@ -415,6 +433,25 @@ 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) { + const moduleContent = this.mechanismClassNameToModuleContent[mechanism.className]; + const allComponents: storageModuleContent.Component[] = [ + ...moduleContent.getComponents(), + ...moduleContent.getPrivateComponents(), + ] + return allComponents; + } + 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..b6738f5a 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", @@ -61,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 c2dbfd82..327ff0f5 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", @@ -62,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 6b5f87b9..6f01d44b 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": "אַף לֹא אֶחָד", @@ -61,7 +62,9 @@ "OPMODE_TYPE": "איזה סוג של מצב פעולה זה", "OPMODE_ENABLED": "האם מצב הפעולה מוצג בתחנת הנהג", "OPMODE_NAME": "השם המוצג בתחנת הנהג. אם ריק ישתמש בשם הכיתה.", - "OPMODE_GROUP": "קבוצה אופציונלית לקיבוץ מצבי פעולה בתחנת הנהג" + "OPMODE_GROUP": "קבוצה אופציונלית לקיבוץ מצבי פעולה בתחנת הנהג", + "COMPONENTS": "רכיבים אלה גלויים במנגנון זה, ברובוט ובכל מצבי הפעולה.", + "PRIVATE_COMPONENTS": "רכיבים אלה לא יהיו גלויים ברובוט או במצבי פעולה. הם נגישים רק בתוך המנגנון הזה." }, "CATEGORY":{ "LISTS": "רשימות", 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 3617e8cf..8a5c3c1c 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'; @@ -77,7 +78,9 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { if (mechanisms.length) { const mechanismBlocks: toolboxItems.Block[] = []; mechanisms.forEach(mechanism => { - const components = editor.getComponentsFromMechanism(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)); }); @@ -109,6 +112,26 @@ 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 => { + // Get the blocks for this specific component. + componentBlocks.push({ + kind: 'category', + name: component.name, + contents: getInstanceMechanismComponentBlocks(component, mechanismInRobot), + }); + }); + mechanismCategories.push({ + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_COMPONENTS'], + contents: componentBlocks, + }); + } + mechanismCategories.push(getMechanismEventHandlersCategory(editor, mechanismInRobot)); contents.push({ @@ -183,8 +206,13 @@ function getComponentsCategory( contents: getAllPossibleComponents(moduleType), }); - // Get components from the current workspace. - editor.getComponentsFromWorkspace().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',