diff --git a/server_python_scripts/blocks_base_classes/mechanism.py b/server_python_scripts/blocks_base_classes/mechanism.py index e9d7ab62..18d32f49 100644 --- a/server_python_scripts/blocks_base_classes/mechanism.py +++ b/server_python_scripts/blocks_base_classes/mechanism.py @@ -1,14 +1,39 @@ # This is the class all mechanisms derive from +from typing import Callable + class Mechanism: def __init__(self): self.hardware = [] + # In self.event_handlers, the keys are the event names, the values are a list of handlers. + self.event_handlers = {} + + def register_event_handler(self, event_name: str, event_handler: Callable) -> None: + if event_name in self.event_handlers: + self.event_handlers[event_name].append(event_handler) + else: + self.event_handlers[event_name] = [event_handler] + + def unregister_event_handler(self, event_name: str, event_handler: Callable) -> None: + if event_name in self.event_handlers: + if event_handler in self.event_handlers[event_name]: + self.event_handlers[event_name].remove(event_handler) + if not self.event_handlers[event_name]: + del self.event_handlers[event_name] + + def fire_event(self, event_name: str, *args) -> None: + if event_name in self.event_handlers: + for event_handler in self.event_handlers[event_name]: + event_handler(*args) + def start(self) -> None: for hardware in self.hardware: hardware.start() + def update(self) -> None: for hardware in self.hardware: hardware.update() + def stop(self) -> None: for hardware in self.hardware: hardware.stop() diff --git a/src/App.tsx b/src/App.tsx index 4b3fa155..89e3ef80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -418,7 +418,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J generatorContext.current.setModule(currentModule); } if (blocksEditor.current) { - blocksEditor.current.loadModuleBlocks(currentModule); + blocksEditor.current.loadModuleBlocks(currentModule, project); } }, [currentModule]); @@ -436,11 +436,15 @@ const AppContent: React.FC = ({ project, setProject }): React.J generatorContext.current.setModule(currentModule); } + if (blocksEditor.current) { + blocksEditor.current.abandon(); + } blocksEditor.current = new editor.Editor(newWorkspace, generatorContext.current, storage); + blocksEditor.current.makeCurrent(); // Set the current module in the editor after creating it if (currentModule) { - blocksEditor.current.loadModuleBlocks(currentModule); + blocksEditor.current.loadModuleBlocks(currentModule, project); } blocksEditor.current.updateToolbox(shownPythonToolboxCategories); diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 24dd2968..f233c01d 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -465,8 +465,8 @@ const CALL_PYTHON_FUNCTION = { } return components; }, - onLoad: function(this: CallPythonFunctionBlock): void { - // onLoad is called for each CallPythonFunctionBlock when the blocks are loaded in the blockly workspace. + mrcOnLoad: function(this: CallPythonFunctionBlock): void { + // mrcOnLoad is called for each CallPythonFunctionBlock when the blocks are loaded in the blockly workspace. const warnings: string[] = []; // If this block is calling a component method, check that the component @@ -476,8 +476,13 @@ const CALL_PYTHON_FUNCTION = { // visible warning on it. if (this.mrcFunctionKind === FunctionKind.INSTANCE_COMPONENT) { let foundComponent = false; - const components = this.getComponentsFromRobot(); - for (const component of components) { + const componentsInScope: commonStorage.Component[] = []; + componentsInScope.push(...this.getComponentsFromRobot()); + const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); + if (editor && editor.getCurrentModuleType() === commonStorage.MODULE_TYPE_MECHANISM) { + componentsInScope.push(...editor.getComponentsFromWorkspace()); + } + for (const component of componentsInScope) { if (component.blockId === this.mrcOtherBlockId) { foundComponent = true; @@ -495,7 +500,7 @@ const CALL_PYTHON_FUNCTION = { } if (indexOfComponentName != -1) { const componentNameChoices : string[] = []; - components.forEach(component => componentNameChoices.push(component.name)); + componentsInScope.forEach(component => componentNameChoices.push(component.name)); titleInput.removeField(FIELD_COMPONENT_NAME); titleInput.insertFieldAt(indexOfComponentName, createFieldDropdown(componentNameChoices), FIELD_COMPONENT_NAME); diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 167f62c7..a884ea28 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -103,8 +103,8 @@ const CLASS_METHOD_DEF = { this.updateBlock_(); }, /** - * Returns the state of this block as a JSON serializable object. - */ + * Returns the state of this block as a JSON serializable object. + */ saveExtraState: function ( this: ClassMethodDefBlock): ClassMethodDefExtraState { const extraState: ClassMethodDefExtraState = { @@ -424,8 +424,11 @@ export const pythonFromBlock = function ( let params = block.mrcParameters; let paramString = "self"; - if (block.mrcPythonMethodName == '__init__') { - paramString += generator.getListOfPorts(false); + if (generator.getModuleType() == commonStorage.MODULE_TYPE_MECHANISM && block.mrcPythonMethodName == '__init__') { + const ports: string[] = generator.getComponentPortParameters(); + if (ports.length) { + paramString += ', ' + ports.join(', '); + } } if (params.length != 0) { diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 717e8f81..5f5a8b0a 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -140,10 +140,13 @@ const COMPONENT = { getComponent: function (this: ComponentBlock): commonStorage.Component | null { const componentName = this.getFieldValue(FIELD_NAME); const componentType = this.getFieldValue(FIELD_TYPE); + const ports: {[port: string]: string} = {}; + this.getComponentPorts(ports); return { blockId: this.id, name: componentName, className: componentType, + ports: ports, }; }, getNewPort: function (this: ComponentBlock, i: number): string { @@ -153,14 +156,12 @@ const COMPONENT = { } return this.getFieldValue(FIELD_NAME) + extension + '_port'; }, - getHardwarePorts: function (this: ComponentBlock, ports: {[key: string]: string}): void { - // Collect the hardware ports for this component block that are needed to generate - // the define_hardware method. (The key is the port, the value is the type.) - if (this.hideParams) { - for (let i = 0; i < this.mrcArgs.length; i++) { - const newPort = this.getNewPort(i); - ports[newPort] = this.mrcArgs[i].type; - } + getComponentPorts: function (this: ComponentBlock, ports: {[key: string]: string}): void { + // Collect the ports for this component block. + for (let i = 0; i < this.mrcArgs.length; i++) { + const newPort = this.getNewPort(i); + // The key is the port, the value is the type. + ports[newPort] = this.mrcArgs[i].type; } }, } @@ -197,7 +198,7 @@ export const pythonFromBlock = function ( return code; } -export function getAllPossibleComponents(hideParams: boolean): toolboxItems.ContentsType[] { +export function getAllPossibleComponents(moduleType: string): toolboxItems.ContentsType[] { const contents: toolboxItems.ContentsType[] = []; // Iterate through all the components subclasses and add definition blocks. const componentTypes = getSubclassNames('component.Component'); @@ -215,7 +216,7 @@ export function getAllPossibleComponents(hideParams: boolean): toolboxItems.Cont classData.staticMethods.forEach(staticFunctionData => { if (staticFunctionData.returnType === componentType) { - contents.push(createComponentBlock(componentName, classData, staticFunctionData, hideParams)); + contents.push(createComponentBlock(componentName, classData, staticFunctionData, moduleType)); } }); }); @@ -224,12 +225,12 @@ export function getAllPossibleComponents(hideParams: boolean): toolboxItems.Cont } function createComponentBlock( - componentName: string, classData: ClassData, staticFunctionData: FunctionData, hideParams: boolean): toolboxItems.Block { + componentName: string, classData: ClassData, staticFunctionData: FunctionData, moduleType: string): toolboxItems.Block { const extraState: ComponentExtraState = { importModule: classData.moduleName, staticFunctionName: staticFunctionData.functionName, params: [], - hideParams: hideParams, + hideParams: (moduleType == commonStorage.MODULE_TYPE_MECHANISM), }; const fields: {[key: string]: any} = {}; fields[FIELD_NAME] = componentName; @@ -241,7 +242,7 @@ function createComponentBlock( 'name': argData.name, 'type': argData.type, }); - if (!hideParams) { + if (moduleType == commonStorage.MODULE_TYPE_ROBOT) { if (argData.type === 'int') { const portType = getPortTypeForArgument(argData.name); if (portType) { diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 7b93d4ac..99905ad5 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -53,7 +53,7 @@ export type EventHandlerBlock = Blockly.Block & EventHandlerMixin & Blockly.Bloc interface EventHandlerMixin extends EventHandlerMixinType { mrcPathOfSender: string; - mrcTypeOfSender: SenderType; + mrcSenderType: SenderType; mrcParameters: Parameter[]; mrcOtherBlockId: string, } @@ -63,7 +63,7 @@ type EventHandlerMixinType = typeof EVENT_HANDLER; /** Extra state for serialising event handler blocks. */ export interface EventHandlerExtraState { pathOfSender: string; - typeOfSender: SenderType; + senderType: SenderType; /** The parameters of the event handler. */ params: Parameter[]; /** The id of the mrc_event block that defines the event. */ @@ -95,7 +95,7 @@ const EVENT_HANDLER = { saveExtraState(this: EventHandlerBlock): EventHandlerExtraState { const extraState: EventHandlerExtraState = { pathOfSender: this.mrcPathOfSender, - typeOfSender: this.mrcTypeOfSender, + senderType: this.mrcSenderType, params: [], otherBlockId: this.mrcOtherBlockId, }; @@ -115,7 +115,7 @@ const EVENT_HANDLER = { */ loadExtraState(this: EventHandlerBlock, extraState: EventHandlerExtraState): void { this.mrcPathOfSender = extraState.pathOfSender; - this.mrcTypeOfSender = extraState.typeOfSender; + this.mrcSenderType = extraState.senderType; this.mrcParameters = []; this.mrcOtherBlockId = extraState.otherBlockId; @@ -158,8 +158,8 @@ const EVENT_HANDLER = { input.removeField(fieldName); }); }, - onLoad: function(this: EventHandlerBlock): void { - // onLoad is called for each EventHandlerBlock when the blocks are loaded in the blockly workspace. + mrcOnLoad: function(this: EventHandlerBlock): void { + // mrcOnLoad is called for each EventHandlerBlock when the blocks are loaded in the blockly workspace. const warnings: string[] = []; // If this block is an event handler for a robot event, check that the robot event @@ -167,7 +167,7 @@ const EVENT_HANDLER = { // If the robot event doesn't exist, put a visible warning on this block. // If the robot event has changed, update the block if possible or put a // visible warning on it. - if (this.mrcTypeOfSender === SenderType.ROBOT) { + if (this.mrcSenderType === SenderType.ROBOT) { let foundRobotEvent = false; const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); if (editor) { @@ -301,7 +301,7 @@ function createRobotEventHandlerBlock( const extraState: EventHandlerExtraState = { // TODO(lizlooney): ask Alan what pathOfSender is for. pathOfSender: '', - typeOfSender: SenderType.ROBOT, + senderType: SenderType.ROBOT, params: [], otherBlockId: event.blockId, }; diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index a80793cb..72e9d40f 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -1,7 +1,7 @@ /** * @license * Copyright 2025 Porpoiseful LLC - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -24,26 +24,35 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_MECHANISMS } from '../themes/styles' import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; +import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { getAllowedTypesForSetCheck } from './utils/python'; +import * as toolboxItems from '../toolbox/items'; +import * as commonStorage from '../storage/common_storage'; +import * as value from './utils/value'; export const BLOCK_NAME = 'mrc_mechanism'; export const OUTPUT_NAME = 'mrc_mechansim'; -type ConstructorArg = { +export const FIELD_NAME = 'NAME'; +export const FIELD_TYPE = 'TYPE'; + +type Parameter = { name: string, type: string, }; type MechanismExtraState = { importModule?: string, - params?: ConstructorArg[], + parameters?: Parameter[], } -export type MechanismBlock = Blockly.Block & MechanismMixin; +const WARNING_ID_MECHANISM_CHANGED = 'mechanism changed'; + +export type MechanismBlock = Blockly.Block & MechanismMixin & Blockly.BlockSvg; interface MechanismMixin extends MechanismMixinType { - mrcArgs: ConstructorArg[], mrcImportModule: string, + mrcParameters: Parameter[], } type MechanismMixinType = typeof MECHANISM; @@ -54,9 +63,9 @@ const MECHANISM = { init: function (this: MechanismBlock): void { this.setStyle(MRC_STYLE_MECHANISMS); this.appendDummyInput() - .appendField(new Blockly.FieldTextInput('my_mech'), 'NAME') + .appendField(new Blockly.FieldTextInput('my_mech'), FIELD_NAME) .appendField(Blockly.Msg.OF_TYPE) - .appendField(createFieldNonEditableText(''), 'TYPE'); + .appendField(createFieldNonEditableText(''), FIELD_TYPE); this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); //this.setOutput(true, OUTPUT_NAME); @@ -68,9 +77,9 @@ const MECHANISM = { saveExtraState: function (this: MechanismBlock): MechanismExtraState { const extraState: MechanismExtraState = { }; - extraState.params = []; - this.mrcArgs.forEach((arg) => { - extraState.params!.push({ + extraState.parameters = []; + this.mrcParameters.forEach((arg) => { + extraState.parameters!.push({ 'name': arg.name, 'type': arg.type, }); @@ -85,36 +94,113 @@ const MECHANISM = { */ loadExtraState: function (this: MechanismBlock, extraState: MechanismExtraState): void { this.mrcImportModule = extraState.importModule ? extraState.importModule : ''; - this.mrcArgs = []; - - if (extraState.params) { - extraState.params.forEach((arg) => { - this.mrcArgs.push({ - 'name': arg.name, - 'type': arg.type, + this.mrcParameters = []; + if (extraState.parameters) { + extraState.parameters.forEach((arg) => { + this.mrcParameters.push({ + name: arg.name, + type: arg.type, }); }); } - this.mrcArgs = extraState.params ? extraState.params : []; 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: MechanismBlock): void { - // Add input sockets for the arguments. - for (let i = 0; i < this.mrcArgs.length; i++) { - const input = this.appendValueInput('ARG' + i) - .setAlign(Blockly.inputs.Align.RIGHT) - .appendField(this.mrcArgs[i].name); - if (this.mrcArgs[i].type) { - input.setCheck(getAllowedTypesForSetCheck(this.mrcArgs[i].type)); + // Update input sockets for the arguments. + for (let i = 0; i < this.mrcParameters.length; i++) { + const argName = this.mrcParameters[i].name; + let argInput = this.getInput('ARG' + i); + const argField = this.getField('ARGNAME' + i); + if (argInput && argField) { + // Ensure argument name is up to date. No need to fire a change event. + Blockly.Events.disable(); + try { + argField.setValue(argName); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + argInput = this.appendValueInput('ARG' + i) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(argName, 'ARGNAME' + i); + } + if (this.mrcParameters[i].type) { + argInput.setCheck(getAllowedTypesForSetCheck(this.mrcParameters[i].type)); } } + // Remove deleted inputs. + for (let i = this.mrcParameters.length; this.getInput('ARG' + i); i++) { + this.removeInput('ARG' + i); + } + }, + getMechanism: function (this: MechanismBlock): commonStorage.MechanismInRobot | null { + const mechanismName = this.getFieldValue(FIELD_NAME); + const mechanismType = this.mrcImportModule + '.' + this.getFieldValue(FIELD_TYPE); + return { + blockId: this.id, + name: mechanismName, + className: mechanismType, + }; }, - getHardwarePorts: function (this: MechanismBlock, _ports: {[key: string]: string}): void { - // TODO: Collect the hardware ports for this mechanism block that are needed to generate - // the robot's define_hardware method. (The key is the port, the value is the type.) + mrcOnLoad: function(this: MechanismBlock): void { + // mrcOnLoad is called for each MechanismBlock when the blocks are loaded in the blockly workspace. + const warnings: string[] = []; + + const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); + if (editor) { + // Find the mechanism. + // TODO(lizlooney): The user can rename the mechanism. We need to store a UUID in + // each module JSON file so we can track mechanisms, etc, even if the name changes. + // Then here, we'd look for the mechanism with the marching UUID, and we'd update the + // FIELD_TYPE value if the mechanism's class name had changed. + let foundMechanism: commonStorage.Mechanism | null = null; + const components: commonStorage.Component[] = [] + for (const mechanism of editor.getMechanisms()) { + if (mechanism.className === this.getFieldValue(FIELD_TYPE)) { + foundMechanism = mechanism; + components.push(...editor.getComponentsFromMechanism(mechanism)); + break; + } + } + + if (foundMechanism) { + // If the mechanism class name has changed, update this blcok. + if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) { + this.setFieldValue(foundMechanism.className, FIELD_TYPE); + } + // If the mechanism module name has changed, update this block. + if (this.mrcImportModule !== foundMechanism.moduleName) { + this.mrcImportModule = foundMechanism.moduleName; + } + this.mrcParameters = []; + components.forEach(component => { + for (const port in component.ports) { + this.mrcParameters.push({ + name: port, + type: component.ports[port], + }); + } + }); + this.updateBlock_(); + } else { + // Did not find the mechanism. + warnings.push('This block refers to a mechanism that no longer exists.'); + } + } + + if (warnings.length) { + // Add a warnings to the block. + const warningText = warnings.join('\n\n'); + this.setWarningText(warningText, WARNING_ID_MECHANISM_CHANGED); + this.bringToFront(); + } else { + // Clear the existing warning on the block. + this.setWarningText(null, WARNING_ID_MECHANISM_CHANGED); + } }, } @@ -129,16 +215,46 @@ export const pythonFromBlock = function ( if (block.mrcImportModule) { generator.addImport(block.mrcImportModule); } - let code = 'self.' + block.getFieldValue('NAME') + ' = ' + block.getFieldValue('TYPE') + '('; + let code = 'self.' + block.getFieldValue(FIELD_NAME) + ' = ' + block.mrcImportModule + '.' + block.getFieldValue(FIELD_TYPE) + '('; - for (let i = 0; i < block.mrcArgs.length; i++) { + for (let i = 0; i < block.mrcParameters.length; i++) { const fieldName = 'ARG' + i; if (i != 0) { code += ', ' } - code += block.mrcArgs[i].name + ' = ' + generator.valueToCode(block, fieldName, Order.NONE); + code += block.mrcParameters[i].name + ' = ' + generator.valueToCode(block, fieldName, Order.NONE); } - code += ')\n' + "self.hardware.append(self." + block.getFieldValue('NAME') + ")\n"; + code += ')\n' + "self.hardware.append(self." + block.getFieldValue(FIELD_NAME) + ")\n"; return code; } + +export function createMechanismBlock( + mechanism: commonStorage.Mechanism, components: commonStorage.Component[]): toolboxItems.Block { + const lastDot = mechanism.className.lastIndexOf('.'); + const mechanismName = ( + 'my_' + + commonStorage.pascalCaseToSnakeCase(mechanism.className.substring(lastDot + 1))); + const extraState: MechanismExtraState = { + importModule: mechanism.moduleName, + parameters: [], + }; + const inputs: {[key: string]: any} = {}; + let i = 0; + components.forEach(component => { + for (const port in component.ports) { + const parameterType = component.ports[port]; + extraState.parameters?.push({ + name: port, + type: parameterType, + }); + const defaultValue = (parameterType === 'int') ? '0' : ''; + inputs['ARG' + i] = value.valueForFunctionArgInput(parameterType, defaultValue); + i++; + } + }); + const fields: {[key: string]: any} = {}; + fields[FIELD_NAME] = mechanismName; + fields[FIELD_TYPE] = mechanism.className; + return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); +} diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index b0932bdf..50128c43 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -26,12 +26,12 @@ import * as ChangeFramework from './utils/change_framework'; import { getLegalName } from './utils/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import * as commonStorage from '../storage/common_storage'; -import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; +import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism'; +import { MechanismBlock } from './mrc_mechanism'; import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component'; import { OUTPUT_NAME as COMPONENT_OUTPUT } from './mrc_component'; import { ComponentBlock } from './mrc_component'; -import { MechanismBlock } from './mrc_mechanism'; import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event'; import { OUTPUT_NAME as EVENT_OUTPUT } from './mrc_event'; import { EventBlock } from './mrc_event'; @@ -132,6 +132,28 @@ const MECHANISM_COMPONENT_HOLDER = { } } }, + getMechanisms: function (this: MechanismComponentHolderBlock): commonStorage.MechanismInRobot[] { + const mechanisms: commonStorage.MechanismInRobot[] = [] + + // Get mechanism blocks from the MECHANISMS input + const mechanismsInput = this.getInput(INPUT_MECHANISMS); + if (mechanismsInput && mechanismsInput.connection) { + // Walk through all connected mechanism blocks. + let mechanismBlock = mechanismsInput.connection.targetBlock(); + while (mechanismBlock) { + if (mechanismBlock.type === MRC_MECHANISM_NAME) { + const mechanism = (mechanismBlock as MechanismBlock).getMechanism(); + if (mechanism) { + mechanisms.push(mechanism); + } + } + // Move to the next block in the chain + mechanismBlock = mechanismBlock.getNextBlock(); + } + } + + return mechanisms; + }, getComponents: function (this: MechanismComponentHolderBlock): commonStorage.Component[] { const components: commonStorage.Component[] = [] @@ -203,28 +225,31 @@ function pythonFromBlockInRobot(block: MechanismComponentHolderBlock, generator: const components = generator.statementToCode(block, INPUT_COMPONENTS); const body = mechanisms + components; - if (body != '') { + if (body) { code += body; generator.addClassMethodDefinition('define_hardware', code); } } function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, generator: ExtendedPythonGenerator) { - let components = ''; - - components = generator.statementToCode(block, INPUT_COMPONENTS); + let code = 'def define_hardware(self'; + const ports: string[] = generator.getComponentPortParameters(); + if (ports.length) { + code += ', ' + ports.join(', '); + } + code += '):\n'; - let code = 'def define_hardware(self' + generator.getListOfPorts(false) + '):\n'; + const components = generator.statementToCode(block, INPUT_COMPONENTS); - if (components != '') { + if (components) { code += components; generator.addClassMethodDefinition('define_hardware', code); } } export const pythonFromBlock = function ( - block: MechanismComponentHolderBlock, - generator: ExtendedPythonGenerator) { + block: MechanismComponentHolderBlock, + generator: ExtendedPythonGenerator) { switch (generator.getModuleType()) { case commonStorage.MODULE_TYPE_ROBOT: pythonFromBlockInRobot(block, generator); @@ -238,42 +263,57 @@ export const pythonFromBlock = function ( // Misc -/** - * Collects the ports for hardware (mechanisms and components). +/**n * Returns true if the given workspace has a mrc_mechanism_component_holder - * block that contains at least one component or mechanism. + * block that contains at least one component. */ -export function getHardwarePorts(workspace: Blockly.Workspace, ports: {[key: string]: string}): boolean { - let hasHardware = false; - workspace.getBlocksByType(BLOCK_NAME).forEach( block => { - const mechanismsInput = block.getInput(INPUT_MECHANISMS); - if (mechanismsInput && mechanismsInput.connection) { - // Walk through all connected mechanism blocks. - let mechanismBlock = mechanismsInput.connection.targetBlock(); - while (mechanismBlock) { - if (mechanismBlock.type === MRC_MECHANISM_NAME && mechanismBlock.isEnabled()) { - hasHardware = true; - (mechanismBlock as MechanismBlock).getHardwarePorts(ports); +export function hasAnyComponents(workspace: Blockly.Workspace): boolean { + for (const block of workspace.getBlocksByType(BLOCK_NAME)) { + const componentsInput = block.getInput(INPUT_COMPONENTS); + if (componentsInput && componentsInput.connection) { + // Walk through all connected component blocks. + let componentBlock = componentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME && componentBlock.isEnabled()) { + return true; } // Move to the next block in the chain - mechanismBlock = mechanismBlock.getNextBlock(); + componentBlock = componentBlock.getNextBlock(); } } + } + return false; +} + +/** + * Collects the ports for components plugged into the mrc_mechanism_component_holder block. + */ +export function getComponentPorts(workspace: Blockly.Workspace, ports: {[key: string]: string}): void { + workspace.getBlocksByType(BLOCK_NAME).forEach( block => { const componentsInput = block.getInput(INPUT_COMPONENTS); if (componentsInput && componentsInput.connection) { // Walk through all connected component blocks. let componentBlock = componentsInput.connection.targetBlock(); while (componentBlock) { if (componentBlock.type === MRC_COMPONENT_NAME && componentBlock.isEnabled()) { - hasHardware = true; - (componentBlock as ComponentBlock).getHardwarePorts(ports); + (componentBlock as ComponentBlock).getComponentPorts(ports); } // Move to the next block in the chain componentBlock = componentBlock.getNextBlock(); } } }); - return hasHardware; +} + +export function getMechanisms( + workspace: Blockly.Workspace, + mechanisms: commonStorage.MechanismInRobot[]): void { + // Get the holder block and ask it for the mechanisms. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + const mechanismsFromHolder: commonStorage.MechanismInRobot[] = + (block as MechanismComponentHolderBlock).getMechanisms(); + mechanisms.push(...mechanismsFromHolder); + }); } export function getComponents( diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 6645c726..cdd14186 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -24,7 +24,6 @@ import * as Blockly from 'blockly/core'; import { extendedPythonGenerator } from './extended_python_generator'; import { GeneratorContext } from './generator_context'; import * as commonStorage from '../storage/common_storage'; -import * as callPythonFunction from '../blocks/mrc_call_python_function'; import * as eventHandler from '../blocks/mrc_event_handler'; import * as classMethodDef from '../blocks/mrc_class_method_def'; import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_holder'; @@ -40,16 +39,19 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { }; export class Editor { - private static workspaceIdToEditor: { [key: string]: Editor } = {}; + private static workspaceIdToEditor: { [workspaceId: string]: Editor } = {}; + private static currentEditor: Editor | null = null; private blocklyWorkspace: Blockly.WorkspaceSvg; private generatorContext: GeneratorContext; private storage: commonStorage.Storage; private currentModule: commonStorage.Module | null = null; + private currentProject: commonStorage.Project | null = null; private modulePath: string = ''; private robotPath: string = ''; private moduleContentText: string = ''; private robotContent: commonStorage.ModuleContent | null = null; + private mechanismClassNameToModuleContent: {[mechanismClassName: string]: commonStorage.ModuleContent} = {}; private bindedOnChange: any = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; @@ -83,10 +85,8 @@ export class Editor { blockCreateEvent.ids.forEach(id => { const block = this.blocklyWorkspace.getBlockById(id); if (block) { - if (block.type == callPythonFunction.BLOCK_NAME) { - (block as callPythonFunction.CallPythonFunctionBlock).onLoad(); - } else if (block.type == eventHandler.BLOCK_NAME) { - (block as eventHandler.EventHandlerBlock).onLoad(); + if ('mrcOnLoad' in block && typeof block.mrcOnLoad === "function") { + block.mrcOnLoad(); } } }); @@ -105,32 +105,54 @@ export class Editor { // TODO(lizlooney): do we need to do anything here? } - public async loadModuleBlocks(currentModule: commonStorage.Module | null) { + public makeCurrent(): void { + Editor.currentEditor = this; + } + + public abandon(): void { + if (Editor.currentEditor === this) { + Editor.currentEditor = null; + } + if (this.blocklyWorkspace.id in Editor.workspaceIdToEditor) { + delete Editor.workspaceIdToEditor[this.blocklyWorkspace.id]; + } + } + + public async loadModuleBlocks( + currentModule: commonStorage.Module | null, + currentProject: commonStorage.Project | null) { this.generatorContext.setModule(currentModule); this.currentModule = currentModule; + this.currentProject = currentProject; - if (currentModule) { - this.modulePath = currentModule.modulePath; - this.robotPath = commonStorage.makeRobotPath(currentModule.projectName); + if (this.currentModule && this.currentProject) { + this.modulePath = this.currentModule.modulePath; + this.robotPath = this.currentProject.robot.modulePath; } else { this.modulePath = ''; this.robotPath = ''; } this.moduleContentText = ''; this.robotContent = null; + this.mechanismClassNameToModuleContent = {} this.clearBlocklyWorkspace(); - if (currentModule) { + if (this.currentModule && this.currentProject) { // Fetch the content for the current module and the robot. // TODO: Also fetch the content for the mechanisms? - const promises: { [key: string]: Promise } = {}; // key is module path, value is promise of module content. + const promises: { [modulePath: string]: Promise } = {}; // value is promise of module content. promises[this.modulePath] = this.storage.fetchModuleContentText(this.modulePath); if (this.robotPath !== this.modulePath) { // Also fetch the robot module content. It contains components, etc, that can be used in OpModes. promises[this.robotPath] = this.storage.fetchModuleContentText(this.robotPath) } + for (const mechanism of this.currentProject.mechanisms) { + if (mechanism.modulePath !== this.modulePath) { + promises[mechanism.modulePath] = this.storage.fetchModuleContentText(mechanism.modulePath) + } + } - const modulePathToContentText: { [key: string]: string } = {}; // key is module path, value is module content + const modulePathToContentText: { [modulePath: string]: string } = {}; // value is module content await Promise.all( Object.entries(promises).map(async ([modulePath, promise]) => { modulePathToContentText[modulePath] = await promise; @@ -142,6 +164,13 @@ export class Editor { } else { this.robotContent = commonStorage.parseModuleContentText(modulePathToContentText[this.robotPath]); } + for (const mechanism of this.currentProject.mechanisms) { + if (mechanism.modulePath === this.modulePath) { + this.mechanismClassNameToModuleContent[mechanism.className] = commonStorage.parseModuleContentText(this.moduleContentText); + } else { + this.mechanismClassNameToModuleContent[mechanism.className] = commonStorage.parseModuleContentText(modulePathToContentText[mechanism.modulePath]); + } + } this.loadBlocksIntoBlocklyWorkspace(); } } @@ -216,6 +245,7 @@ export class Editor { extendedPythonGenerator.mrcWorkspaceToCode(this.blocklyWorkspace, this.generatorContext); const blocks = Blockly.serialization.workspaces.save(this.blocklyWorkspace); + const mechanisms: commonStorage.MechanismInRobot[] = this.getMechanismsFromWorkspace(); const components: commonStorage.Component[] = this.getComponentsFromWorkspace(); const events: commonStorage.Event[] = this.getEventsFromWorkspace(); const methods: commonStorage.Method[] = ( @@ -224,7 +254,15 @@ export class Editor { ? this.getMethodsForOutsideFromWorkspace() : []; return commonStorage.makeModuleContentText( - this.currentModule, blocks, components, events, methods); + this.currentModule, blocks, mechanisms, components, events, methods); + } + + public getMechanismsFromWorkspace(): commonStorage.MechanismInRobot[] { + const mechanisms: commonStorage.MechanismInRobot[] = []; + if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { + mechanismComponentHolder.getMechanisms(this.blocklyWorkspace, mechanisms); + } + return mechanisms; } public getComponentsFromWorkspace(): commonStorage.Component[] { @@ -280,6 +318,19 @@ export class Editor { } } + /** + * Returns the mechanisms defined in the robot. + */ + public getMechanismsFromRobot(): commonStorage.MechanismInRobot[] { + if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { + return this.getMechanismsFromWorkspace(); + } + if (this.robotContent) { + return this.robotContent.getMechanisms(); + } + throw new Error('getMechanismsFromRobot: this.robotContent is null.'); + } + /** * Returns the components defined in the robot. */ @@ -287,10 +338,10 @@ export class Editor { if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return this.getComponentsFromWorkspace(); } - if (!this.robotContent) { - throw new Error('getComponentsFromRobot: this.robotContent is null.'); + if (this.robotContent) { + return this.robotContent.getComponents(); } - return this.robotContent.getComponents(); + throw new Error('getComponentsFromRobot: this.robotContent is null.'); } /** @@ -300,10 +351,10 @@ export class Editor { if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return this.getEventsFromWorkspace(); } - if (!this.robotContent) { - throw new Error('getEventsFromRobot: this.robotContent is null.'); + if (this.robotContent) { + return this.robotContent.getEvents(); } - return this.robotContent.getEvents(); + throw new Error('getEventsFromRobot: this.robotContent is null.'); } /** @@ -313,10 +364,56 @@ export class Editor { if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return this.getMethodsForWithinFromWorkspace(); } - if (!this.robotContent) { - throw new Error('getMethodsFromRobot: this.robotContent is null.'); + if (this.robotContent) { + return this.robotContent.getMethods(); } - return this.robotContent.getMethods(); + throw new Error('getMethodsFromRobot: this.robotContent is null.'); + } + + /** + * Returns the mechanisms in this project. + */ + public getMechanisms(): commonStorage.Mechanism[] { + return this.currentProject ? this.currentProject.mechanisms : []; + } + + /** + * Returns the components defined in the given mechanism. + */ + public getComponentsFromMechanism(mechanism: commonStorage.Mechanism): commonStorage.Component[] { + if (this.currentModule?.modulePath === mechanism.modulePath) { + return this.getComponentsFromWorkspace(); + } + if (mechanism.className in this.mechanismClassNameToModuleContent) { + return this.mechanismClassNameToModuleContent[mechanism.className].getComponents(); + } + throw new Error('getComponentsFromMechanism: mechanism not found: ' + mechanism.className); + } + + /** + * Returns the events defined in the given mechanism. + */ + public getEventsFromMechanism(mechanism: commonStorage.Mechanism): commonStorage.Event[] { + if (this.currentModule?.modulePath === mechanism.modulePath) { + return this.getEventsFromWorkspace(); + } + if (mechanism.className in this.mechanismClassNameToModuleContent) { + return this.mechanismClassNameToModuleContent[mechanism.className].getEvents(); + } + throw new Error('getEventsFromMechanism: mechanism not found: ' + mechanism.className); + } + + /** + * Returns the methods defined in the given mechanism. + */ + public getMethodsFromMechanism(mechanism: commonStorage.Mechanism): commonStorage.Method[] { + if (this.currentModule?.modulePath === mechanism.modulePath) { + return this.getMethodsForWithinFromWorkspace(); + } + if (mechanism.className in this.mechanismClassNameToModuleContent) { + return this.mechanismClassNameToModuleContent[mechanism.className].getMethods(); + } + throw new Error('getMethodsFromMechanism: mechanism not found: ' + mechanism.className); } public static getEditorForBlocklyWorkspace(workspace: Blockly.Workspace): Editor | null { @@ -324,12 +421,19 @@ export class Editor { return Editor.workspaceIdToEditor[workspace.id]; } - // If the workspace id was not found, it might be because the workspace is associated with the - // toolbox flyout, not a real workspace. In that case, use the first editor. - const allEditors = Object.values(Editor.workspaceIdToEditor); - if (allEditors.length) { - return allEditors[0]; + // If the workspace id was not found, it might be because the workspace is associated with a block mutator's flyout. + // Try this workspaces's root workspace. + const rootWorkspace = workspace.getRootWorkspace(); + if (rootWorkspace && + rootWorkspace.id in Editor.workspaceIdToEditor) { + return Editor.workspaceIdToEditor[rootWorkspace.id]; } - return null; + + // Otherwise, return the current editor. + return Editor.currentEditor; + } + + public static getCurrentEditor(): Editor | null { + return Editor.currentEditor; } } diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index eb92d0e7..6088563a 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -71,12 +71,12 @@ export class ExtendedPythonGenerator extends PythonGenerator { private workspace: Blockly.Workspace | null = null; private context: GeneratorContext | null = null; - // Has components or mechanisms (ie, might need to call self.define_hardware in __init__) - private hasHardware = false; - private ports: {[key: string]: string} = Object.create(null); + // Fields related to generating the __init__ for a mechanism. + private hasAnyComponents = false; + private componentPorts: {[key: string]: string} = Object.create(null); // Has event handlers (ie, needs to call self.register_event_handlers in __init__) - private hasEventHandler = false; + private hasAnyEventHandlers = false; private classMethods: {[key: string]: string} = Object.create(null); // For eventHandlers, the keys are the function name. @@ -112,22 +112,12 @@ export class ExtendedPythonGenerator extends PythonGenerator { generateInitStatements() : string { let initStatements = ''; - if (this.hasHardware) { - switch (this.getModuleType()) { - case commonStorage.MODULE_TYPE_ROBOT: - // The RobotBase __init__ method already calls self.define_robot(), so - // we don't need to generate that call here. - break; - case commonStorage.MODULE_TYPE_MECHANISM: - // For mechanisms - initStatements += this.INDENT + "self.define_hardware("; - initStatements += this.getListOfPorts(true); - initStatements += ')\n'; - break; - } + if (this.getModuleType() === commonStorage.MODULE_TYPE_MECHANISM && this.hasAnyComponents) { + initStatements += this.INDENT + 'self.define_hardware(' + + this.getComponentPortParameters().join(', ') + ')\n'; } - if (this.hasEventHandler) { + if (this.hasAnyEventHandlers) { initStatements += this.INDENT + "self.register_event_handlers()\n"; } @@ -143,9 +133,13 @@ export class ExtendedPythonGenerator extends PythonGenerator { this.workspace = workspace; this.context = context; - this.ports = Object.create(null); - this.hasHardware = mechanismContainerHolder.getHardwarePorts(this.workspace, this.ports); - this.hasEventHandler = eventHandler.getHasAnyEnabledEventHandlers(this.workspace); + this.hasAnyComponents = false; + this.componentPorts = Object.create(null); + if (this.getModuleType() === commonStorage.MODULE_TYPE_MECHANISM) { + this.hasAnyComponents = mechanismContainerHolder.hasAnyComponents(this.workspace); + mechanismContainerHolder.getComponentPorts(this.workspace, this.componentPorts); + } + this.hasAnyEventHandlers = eventHandler.getHasAnyEnabledEventHandlers(this.workspace); const code = super.workspaceToCode(workspace); @@ -193,19 +187,12 @@ export class ExtendedPythonGenerator extends PythonGenerator { } } - getListOfPorts(startWithFirst: boolean): string{ - let returnString = '' - let firstPort = startWithFirst; - for (const port in this.ports) { - if (!firstPort){ - returnString += ', '; - } - else{ - firstPort = false; - } - returnString += port; + getComponentPortParameters(): string[] { + const ports: string[] = [] + for (const port in this.componentPorts) { + ports.push(port); } - return returnString; + return ports; } finish(code: string): string { @@ -248,7 +235,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { } this.eventHandlers = Object.create(null); this.classMethods = Object.create(null); - this.ports = Object.create(null); + this.componentPorts = Object.create(null); code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); if (decorations){ code = decorations + code; diff --git a/src/modules/mechanism_start.json b/src/modules/mechanism_start.json index d3f6a04b..c2f2488c 100644 --- a/src/modules/mechanism_start.json +++ b/src/modules/mechanism_start.json @@ -7,6 +7,7 @@ "id": "ElbONc{)s(,UprTW(|1C", "x": 10, "y": 210, + "editable": false, "deletable": false, "extraState": { "canChangeSignature": false, @@ -25,6 +26,7 @@ "id": "wxFAh6eaR1|3fTuV:UAd", "x": 10, "y": 410, + "editable": false, "deletable": false, "extraState": { "canChangeSignature": false, @@ -42,6 +44,7 @@ "id": "ElbOAb{)s(,UprTW(|1C", "x": 10, "y": 10, + "editable": false, "deletable": false, "extraState":{ "hideMechanisms" : true diff --git a/src/modules/opmode_start.json b/src/modules/opmode_start.json index 09c56427..dd9fa322 100644 --- a/src/modules/opmode_start.json +++ b/src/modules/opmode_start.json @@ -7,6 +7,7 @@ "id": "ElbONc{)gh,UprTW(|1C", "x": 10, "y": 10, + "editable": false, "deletable": false, "fields": { "NAME": "", @@ -20,6 +21,7 @@ "id": "ElbONc{)s(,UprTW(|1C", "x": 10, "y": 120, + "editable": false, "deletable": false, "extraState": { "canChangeSignature": false, @@ -43,6 +45,7 @@ "id": "wxFAh6eaR1|3fTuV:UAd", "x": 10, "y": 300, + "editable": false, "deletable": false, "extraState": { "canChangeSignature": false, diff --git a/src/modules/robot_start.json b/src/modules/robot_start.json index 1a54456b..6cb4a3d4 100644 --- a/src/modules/robot_start.json +++ b/src/modules/robot_start.json @@ -7,6 +7,7 @@ "id": "ElbONc{)s(,UprTW(|1C", "x": 10, "y": 210, + "editable": false, "deletable": false, "extraState": { "canChangeSignature": false, @@ -25,6 +26,7 @@ "id": "ElbOAb{)s(,UprTW(|1C", "x": 10, "y": 10, + "editable": false, "deletable": false } ] diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index ebcc28aa..e71b387f 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -138,8 +138,7 @@ class ClientSideStorage implements commonStorage.Storage { if (cursor) { const value = cursor.value; const path = value.path; - // Before PR #143, robot modules were stored with the type 'project'. - const moduleType = (value.type == 'project') ? commonStorage.MODULE_TYPE_ROBOT : value.type; + const moduleType = value.type; const moduleName = commonStorage.getModuleName(path); const module: commonStorage.Module = { modulePath: path, diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 557853f7..63546973 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -28,6 +28,7 @@ import startingRobotBlocks from '../modules/robot_start.json'; // Types, constants, and functions related to modules, regardless of where the modules are stored. export type Module = { + // TODO(lizlooney): Add a uuid so we can keep track of mechanisms in the robot even if the user renames the mechamism modulePath: string, moduleType: string, projectName: string, @@ -61,10 +62,17 @@ export type Method = { args: MethodArg[], }; +export type MechanismInRobot = { + blockId: string, // ID of the mrc_mechanism block that adds the mechanism to the robot. + name: string, + className: string, +} + export type Component = { - blockId: string, // ID of the mrc_component block that defines the component. + blockId: string, // ID of the mrc_component block that adds the component to the robot or to a mechanism. name: string, className: string, + ports: {[port: string]: string}, // The value is the type. } export type Event = { @@ -483,12 +491,14 @@ export function getModuleName(modulePath: string): string { function startingBlocksToModuleContentText( module: Module, startingBlocks: { [key: string]: any }): string { + const mechanisms: MechanismInRobot[] = []; const components: Component[] = []; const events: Event[] = []; const methods: Method[] = []; return makeModuleContentText( module, startingBlocks, + mechanisms, components, events, methods); @@ -548,12 +558,14 @@ export function newOpModeContent(projectName: string, opModeName: string): strin export function makeModuleContentText( module: Module, blocks: { [key: string]: any }, + mechanisms: MechanismInRobot[], components: Component[], events: Event[], methods: Method[]): string { const moduleContent = new ModuleContent( module.moduleType, blocks, + mechanisms, components, events, methods); @@ -565,6 +577,7 @@ export function parseModuleContentText(moduleContentText: string): ModuleContent return new ModuleContent( parsedContent.moduleType, parsedContent.blocks, + parsedContent.mechanisms, parsedContent.components, parsedContent.events, parsedContent.methods); @@ -574,6 +587,7 @@ export class ModuleContent { constructor( private moduleType: string, private blocks : { [key: string]: any }, + private mechanisms: MechanismInRobot[], private components: Component[], private events: Event[], private methods: Method[]) { @@ -591,6 +605,10 @@ export class ModuleContent { return this.blocks; } + getMechanisms(): MechanismInRobot[] { + return this.mechanisms; + } + getComponents(): Component[] { return this.components; } diff --git a/src/toolbox/blocks_mechanisms.ts b/src/toolbox/blocks_mechanisms.ts deleted file mode 100644 index 494cc595..00000000 --- a/src/toolbox/blocks_mechanisms.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright 2025 Porpoiseful LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @author alan@porpoiseful.com (Alan Smith) - */ -import * as ToolboxItems from './items'; - -export function getAllPossibleMechanisms(): ToolboxItems.ContentsType[] { - const contents: ToolboxItems.ContentsType[] = [] - - // TODO: Get the blocks for adding mechanisms. - - /* // Uncomment this fake code for testing purposes only. - contents.push( - { - kind: 'block', - type: 'mrc_mechanism', - fields: { - NAME: 'claw', - TYPE: 'Claw' - }, - extraState: { - importModule: 'claw', - params: [ - { name: 'gripper_port', type: 'int' }, - { name: 'piece_sensor_port', type: 'int' }, - ] - }, - inputs: { - ARG0: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartMotor', - PORT_NUM: 1 - }, - }, - }, - ARG1: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartIO', - PORT_NUM: 1 - }, - }, - }, - } - }, - { - kind: 'block', - type: 'mrc_mechanism', - fields: { - NAME: 'drive', - TYPE: 'DriveMecanum' - }, - extraState: { - importModule: 'drive_mecanum', - params: [ - { name: 'front_left_drive_port', type: 'int' }, - { name: 'front_right_drive_port', type: 'int' }, - { name: 'back_left_drive_port', type: 'int' }, - { name: 'back_right_drive_port', type: 'int' }, - ] - }, - inputs: { - ARG0: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartMotor', - PORT_NUM: 1 - }, - }, - }, - ARG1: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartMotor', - PORT_NUM: 2 - }, - }, - }, - ARG2: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartMotor', - PORT_NUM: 3 - }, - }, - }, - ARG3: { - shadow: { - type: 'mrc_port', - fields: { - TYPE: 'SmartMotor', - PORT_NUM: 4 - }, - }, - }, - } - } - ); - */ - - return contents; -} diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index 035f0eec..5885fc31 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -22,37 +22,36 @@ import * as Blockly from 'blockly/core'; import * as commonStorage from '../storage/common_storage'; import * as toolboxItems from './items'; -import { getAllPossibleMechanisms } from './blocks_mechanisms'; +import { createMechanismBlock } from '../blocks/mrc_mechanism'; import { getAllPossibleComponents } from '../blocks/mrc_component'; import { getInstanceComponentBlocks, addInstanceRobotBlocks } from '../blocks/mrc_call_python_function'; import { addRobotEventHandlerBlocks } from '../blocks/mrc_event_handler'; import { Editor } from '../editor/editor'; export function getHardwareCategory(currentModule: commonStorage.Module): toolboxItems.Category { - if (currentModule.moduleType === commonStorage.MODULE_TYPE_ROBOT) { - return { - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_HARDWARE'], - contents: [ - getRobotMechanismsCategory(currentModule), - getComponentsCategory(false), - ] - }; - } - if (currentModule.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - return getComponentsCategory(true); - } - if (currentModule.moduleType === commonStorage.MODULE_TYPE_OPMODE) { - return { - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_ROBOT'], - contents: [ - getRobotMechanismsCategory(currentModule), - getRobotComponentsCategory(), - getRobotMethodsCategory(), - getRobotEventsCategory(), - ] - }; + switch (currentModule.moduleType) { + case commonStorage.MODULE_TYPE_ROBOT: + return { + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_HARDWARE'], + contents: [ + getRobotMechanismsCategory(currentModule), + getComponentsCategory(currentModule.moduleType), + ] + }; + case commonStorage.MODULE_TYPE_MECHANISM: + return getComponentsCategory(currentModule.moduleType); + case commonStorage.MODULE_TYPE_OPMODE: + return { + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_ROBOT'], + contents: [ + getRobotMechanismsCategory(currentModule), + getRobotComponentsCategory(), + getRobotMethodsCategory(), + getRobotEventsCategory(), + ] + }; } throw new Error('currentModule.moduleType has unexpected value: ' + currentModule.moduleType) } @@ -66,17 +65,39 @@ function getRobotMechanismsCategory(currentModule: commonStorage.Module): toolbo const contents: toolboxItems.ContentsType[] = []; - // Include the "+ Mechanism" category if the user it editing the robot. + const editor = Editor.getCurrentEditor(); + + // Include the "+ Mechanism" category if the user it editing the robot and there are any mechanism modules. if (currentModule.moduleType === commonStorage.MODULE_TYPE_ROBOT) { - contents.push({ - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_ADD_MECHANISM'], - contents: getAllPossibleMechanisms(), - }); + if (editor) { + const mechanisms = editor.getMechanisms(); + if (mechanisms.length) { + const mechanismBlocks: toolboxItems.Block[] = []; + mechanisms.forEach(mechanism => { + const components = editor.getComponentsFromMechanism(mechanism); + mechanismBlocks.push(createMechanismBlock(mechanism, components)); + }); + + contents.push({ + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_ADD_MECHANISM'], + contents: mechanismBlocks, + }); + } + } } - // TODO: Get the list of mechanisms from the robot and add the blocks for - // calling the mechanism functions to the toolbox. + if (editor) { + editor.getMechanismsFromRobot().forEach(mechanism => { + const mechanismBlocks: toolboxItems.Block[] = []; + // TODO(lizlooney): add the blocks for mechanism methods. + contents.push({ + kind: 'category', + name: mechanism.name, + contents: mechanismBlocks, + }); + }); + } /* // Uncomment this fake code for testing purposes only. contents.push( @@ -217,22 +238,20 @@ function getRobotComponentsCategory(): toolboxItems.Category { const contents: toolboxItems.ContentsType[] = []; + const editor = Editor.getCurrentEditor(); + // Get the list of components from the robot and add the blocks for calling the // component functions. - const workspace = Blockly.getMainWorkspace(); - if (workspace) { - const editor = Editor.getEditorForBlocklyWorkspace(workspace); - if (editor) { - const componentsFromRobot = editor.getComponentsFromRobot(); - componentsFromRobot.forEach(component => { - // Get the blocks for this specific component. - contents.push({ - kind: 'category', - name: component.name, - contents: getInstanceComponentBlocks(component), - }); + if (editor) { + const componentsFromRobot = editor.getComponentsFromRobot(); + componentsFromRobot.forEach(component => { + // Get the blocks for this specific component. + contents.push({ + kind: 'category', + name: component.name, + contents: getInstanceComponentBlocks(component), }); - } + }); } return { @@ -250,13 +269,10 @@ function getRobotMethodsCategory(): toolboxItems.Category { // Get the list of methods from the robot and add the blocks for calling the // robot functions. - const workspace = Blockly.getMainWorkspace(); - if (workspace) { - const editor = Editor.getEditorForBlocklyWorkspace(workspace); - if (editor) { - const methodsFromRobot = editor.getMethodsFromRobot(); - addInstanceRobotBlocks(methodsFromRobot, contents); - } + const editor = Editor.getCurrentEditor(); + if (editor) { + const methodsFromRobot = editor.getMethodsFromRobot(); + addInstanceRobotBlocks(methodsFromRobot, contents); } return { @@ -266,7 +282,7 @@ function getRobotMethodsCategory(): toolboxItems.Category { }; } -function getComponentsCategory(hideParams : boolean): toolboxItems.Category { +function getComponentsCategory(moduleType : string): toolboxItems.Category { // getComponentsCategory is called when the user is editing the robot or a // mechanism. It allows the user to add a component or use an existing component. @@ -276,24 +292,21 @@ function getComponentsCategory(hideParams : boolean): toolboxItems.Category { contents.push({ kind: 'category', name: Blockly.Msg['MRC_CATEGORY_ADD_COMPONENT'], - contents: getAllPossibleComponents(hideParams) + contents: getAllPossibleComponents(moduleType), }); // Get components from the current workspace. - const workspace = Blockly.getMainWorkspace(); - if (workspace) { - const editor = Editor.getEditorForBlocklyWorkspace(workspace); - if (editor) { - const componentsFromWorkspace = editor.getComponentsFromWorkspace(); - componentsFromWorkspace.forEach(component => { - // Get the blocks for this specific component - contents.push({ - kind: 'category', - name: component.name, - contents: getInstanceComponentBlocks(component), - }); + const editor = Editor.getCurrentEditor(); + if (editor) { + const componentsFromWorkspace = editor.getComponentsFromWorkspace(); + componentsFromWorkspace.forEach(component => { + // Get the blocks for this specific component + contents.push({ + kind: 'category', + name: component.name, + contents: getInstanceComponentBlocks(component), }); - } + }); } return {