diff --git a/src/App.tsx b/src/App.tsx index 3fd2429e..7fe674ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,6 +49,7 @@ import { initialize as initializeGeneratedBlocks } from './blocks/utils/generate import * as editor from './editor/editor'; import { extendedPythonGenerator } from './editor/extended_python_generator'; +import { createGeneratorContext, GeneratorContext } from './editor/generator_context'; import * as toolboxItems from './toolbox/items'; import * as toolbox from './toolbox/toolbox'; @@ -252,6 +253,7 @@ const App: React.FC = () => { const [deleteTooltip, setDeleteTooltip] = useState('Delete'); const blocklyComponent = useRef(null); const [triggerPythonRegeneration, setTriggerPythonRegeneration] = useState(0); + const generatorContext = useRef(null); const blocksEditor = useRef(null); const [generatedCode, setGeneratedCode] = useState(''); const [newProjectNameModalPurpose, setNewProjectNameModalPurpose] = useState(''); @@ -461,6 +463,9 @@ const App: React.FC = () => { ? commonStorage.findModule(modules, currentModulePath) : null; setCurrentModule(module); + if (generatorContext.current) { + generatorContext.current.setModule(module); + } if (module != null) { if (module.moduleType == commonStorage.MODULE_TYPE_PROJECT) { @@ -498,10 +503,12 @@ const App: React.FC = () => { if (ignoreEffect()) { return; } - if (blocklyComponent.current) { + if (currentModule && blocklyComponent.current && generatorContext.current) { const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); - extendedPythonGenerator.setCurrentModule(currentModule); - setGeneratedCode(extendedPythonGenerator.workspaceToCode(blocklyWorkspace)); + setGeneratedCode(extendedPythonGenerator.workspaceToCode( + blocklyWorkspace, generatorContext.current)); + } else { + setGeneratedCode(''); } }, [currentModule, triggerPythonRegeneration, blocklyComponent]); @@ -527,7 +534,8 @@ const App: React.FC = () => { blocklyWorkspace.addChangeListener(mutatorOpenListener); blocklyWorkspace.addChangeListener(handleBlocksChanged); } - blocksEditor.current = new editor.Editor(blocklyWorkspace, storage); + generatorContext.current = createGeneratorContext(); + blocksEditor.current = new editor.Editor(blocklyWorkspace, generatorContext.current, storage); }, [blocklyComponent, storage]); const handleBlocksChanged = (event: Blockly.Events.Abstract) => { diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 1bb367e6..2323b8dc 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -389,7 +389,7 @@ export const pythonFromBlock = function ( } let params = block.mrcParameters; - let paramString = "self" + let paramString = "self"; if (params.length != 0) { block.mrcParameters.forEach((param) => { paramString += ', ' + param.name; @@ -409,7 +409,7 @@ export const pythonFromBlock = function ( xfix2 + returnValue; code = generator.scrub_(block, code); - generator.addMethod(funcName, code); + generator.addClassMethodDefinition(block.getFieldValue('NAME'), funcName, code); - return code; + return ''; } \ No newline at end of file diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 6530e35f..d6832e8f 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -22,6 +22,7 @@ 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 { getToolboxJSON } from '../toolbox/toolbox'; @@ -33,6 +34,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { export class Editor { private blocklyWorkspace: Blockly.WorkspaceSvg; + private generatorContext: GeneratorContext; private storage: commonStorage.Storage; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; @@ -42,8 +44,9 @@ export class Editor { private bindedOnChange: any = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; - constructor(blocklyWorkspace: Blockly.WorkspaceSvg, storage: commonStorage.Storage) { + constructor(blocklyWorkspace: Blockly.WorkspaceSvg, generatorContext: GeneratorContext, storage: commonStorage.Storage) { this.blocklyWorkspace = blocklyWorkspace; + this.generatorContext = generatorContext; this.storage = storage; } @@ -104,6 +107,7 @@ export class Editor { } public async loadModuleBlocks(currentModule: commonStorage.Module | null) { + this.generatorContext.setModule(currentModule); this.currentModule = currentModule; if (currentModule) { this.modulePath = currentModule.modulePath; @@ -203,9 +207,8 @@ export class Editor { } private getModuleContent(): string { - extendedPythonGenerator.setCurrentModule(this.currentModule); - const pythonCode = extendedPythonGenerator.workspaceToCode(this.blocklyWorkspace); - const exportedBlocks = JSON.stringify(extendedPythonGenerator.getExportedBlocks(this.blocklyWorkspace)); + const pythonCode = extendedPythonGenerator.workspaceToCode(this.blocklyWorkspace, this.generatorContext); + const exportedBlocks = JSON.stringify(this.generatorContext.getExportedBlocks()); const blocksContent = JSON.stringify(Blockly.serialization.workspaces.save(this.blocklyWorkspace)); return commonStorage.makeModuleContent(this.currentModule, pythonCode, exportedBlocks, blocksContent); } diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 4ece77f7..b9275551 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -21,7 +21,8 @@ import * as Blockly from 'blockly/core'; import { PythonGenerator } from 'blockly/python'; -import { Block } from "../toolbox/items"; +import { GeneratorContext } from './generator_context'; +import { Block } from '../toolbox/items'; import { FunctionArg } from '../blocks/mrc_call_python_function'; import * as commonStorage from '../storage/common_storage'; @@ -29,22 +30,62 @@ import * as commonStorage from '../storage/common_storage'; // variables that have been defined so they can be used in other modules. export class ExtendedPythonGenerator extends PythonGenerator { - private currentModule: commonStorage.Module | null = null; - private mapWorkspaceIdToExportedBlocks: { [key: string]: Block[] } = Object.create(null); - protected methods_: {[key: string]: string} = Object.create(null); + private workspace: Blockly.Workspace | null = null; + private context: GeneratorContext | null = null; + private classMethods: {[key: string]: string} = Object.create(null); constructor() { super('Python'); } - setCurrentModule(module: commonStorage.Module | null) { - this.currentModule = module; + workspaceToCode(workspace: Blockly.Workspace, context: GeneratorContext): string { + this.workspace = workspace; + this.context = context; + this.context.clear(); + + const code = super.workspaceToCode(workspace); + + this.workspace = workspace; + this.context = null; + return code; } - init(workspace: Blockly.Workspace) { - super.init(workspace); + /** + * Add an import statement for a python module. + */ + addImport(importModule: string): void { + this.definitions_['import_' + importModule] = 'import ' + importModule; + } + + /** + * Add a class method definition. + */ + addClassMethodDefinition(nameFieldValue: string, methodName: string, code: string): void { + this.context.addClassMethodName(nameFieldValue, methodName); + this.classMethods[methodName] = code; + } + + finish(code: string): string { + if (this.context) { + const className = this.context.getClassName(); + const classParent = this.context.getClassParent(); + this.addImport(classParent); + const classDef = 'class ' + className + '(' + classParent + '):\n'; + const classMethods = []; + for (let name in this.classMethods) { + classMethods.push(this.classMethods[name]) + } + this.classMethods = Object.create(null); + code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); + + this.context.setExportedBlocks(this.produceExportedBlocks(this.workspace)); + } + + return super.finish(code); + } + private produceExportedBlocks(workspace: Blockly.Workspace): Block[] { // The exported blocks produced here have the extraState.importModule and fields.MODULE values // set to the MODULE_NAME_PLACEHOLDER. This is so blocks modules can be renamed and copied // without having to change the contents of the modules. @@ -54,6 +95,8 @@ export class ExtendedPythonGenerator extends PythonGenerator { const exportedBlocks = []; // All functions are exported. + // TODO(lizlooney): instead of looking at procedure blocks, this code needs + // to look at mrc_class_method_def blocks. const allProcedures = Blockly.Procedures.allProcedures(workspace); const procedureTuples = allProcedures[0].concat(allProcedures[1]); for (const procedureTuple of procedureTuples) { @@ -149,74 +192,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { exportedBlocks.push(setPythonModuleVariableBlock); } } - this.mapWorkspaceIdToExportedBlocks[workspace.id] = exportedBlocks; - } - - getExportedBlocks(workspace: Blockly.Workspace): Block[] { - return this.mapWorkspaceIdToExportedBlocks[workspace.id]; - } - - // Functions used in python code generation for multiple python blocks. - addImport(importModule: string): void { - this.definitions_['import_' + importModule] = 'import ' + importModule; - } - addMethod(methodName: string, code : string): void { - this.methods_[methodName] = code; - } - - classParentFromModuleType(moduleType : string) : string{ - if(moduleType == commonStorage.MODULE_TYPE_PROJECT){ - return "RobotBase"; - } - if(moduleType == commonStorage.MODULE_TYPE_OPMODE){ - return "OpMode"; - } - if(moduleType == commonStorage.MODULE_TYPE_MECHANISM){ - return "Mechanism"; - } - return ""; - } - - finish(code: string): string { - if (!this.currentModule) { - return super.finish(code); - } - let className = 'Robot'; // Default for Workspace - if (this.currentModule.moduleType != commonStorage.MODULE_TYPE_WORKSPACE){ - className = this.currentModule.moduleName; - } - let classParent = this.classParentFromModuleType(this.currentModule.moduleType); - this.addImport(classParent); - - // Convert the definitions dictionary into a list. - const imports = []; - const definitions = []; - - for (let name in this.definitions_) { - const def = this.definitions_[name]; - if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) { - imports.push(def); - } else{ - definitions.push(def); - } - } - const methods = []; - for (let name in this.methods_){ - methods.push(this.methods_[name]) - } - - this.definitions_ = Object.create(null); - this.functionNames_ = Object.create(null); - this.methods_ = Object.create(null); - - this.isInitialized = false; - - let class_def = "class " + className + "(" + classParent + "):\n"; - - this.nameDB_!.reset(); - const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); - return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + class_def + - this.prefixLines(methods.join('\n\n'), this.INDENT); + return exportedBlocks; } } diff --git a/src/editor/generator_context.ts b/src/editor/generator_context.ts new file mode 100644 index 00000000..2616c74b --- /dev/null +++ b/src/editor/generator_context.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2025 Google 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 lizlooney@google.com (Liz Looney) + */ + +import { Block } from "../toolbox/items"; +import * as commonStorage from '../storage/common_storage'; + + +export function createGeneratorContext(): GeneratorContext { + return new GeneratorContext(); +} + +export class GeneratorContext { + private module: commonStorage.Module | null = null; + + // The exported blocks for the current module. + private exportedBlocks: Block[] = []; + + // Key is the mrc_class_method_def block's NAME field, value is the python method name. + private classMethodNames: {[key: string]: string} = Object.create(null); + + setModule(module: commonStorage.Module | null) { + this.module = module; + this.clear(); + } + + clear(): void { + this.clearExportedBlocks(); + this.clearClassMethodNames(); + } + + getClassName(): string { + if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) { + return 'Robot'; + } + + // TODO(lizlooney): className should be a field in commonStorage.Module. + // Until that happens, we'll figure it out now from the module name. + + let className = ''; + let nextCharUpper = true; + for (let i = 0; i < this.module.moduleName.length; i++) { + const char = this.module.moduleName.charAt(i); + if (char !== '_') { + className += nextCharUpper ? char.toUpperCase() : char; + } + nextCharUpper = (char === '_'); + } + return className; + } + + getClassParent(): string { + if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) { + return 'RobotBase'; + } + if (this.module.moduleType === commonStorage.MODULE_TYPE_OPMODE) { + return 'OpMode'; + } + if (this.module.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { + return 'Mechanism'; + } + return ''; + } + + clearExportedBlocks() { + this.exportedBlocks.length = 0; + } + + setExportedBlocks(exportedBlocks: Blocks[]) { + this.exportedBlocks.length = 0; + this.exportedBlocks.push(...exportedBlocks); + } + + getExportedBlocks(): Block[] { + return this.exportedBlocks; + } + + clearClassMethodNames() { + this.classMethodNames = Object.create(null); + } + + addClassMethodName(nameFieldValue: string, methodName: string) { + if (nameFieldValue !== methodName) { + this.classMethodNames[nameFieldValue] = methodName; + } + } + + getClassMethodName(nameFieldValue: string): string | null { + if (this.classMethodNames[nameFieldValue]) { + return this.classMethodNames[nameFieldValue]; + } + return nameFieldValue; + } +} diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 4d13bf6d..04355845 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -29,6 +29,7 @@ import startingMechanismBlocks from '../modules/mechanism_start.json'; import startingRobotBlocks from '../modules/robot_start.json'; import {extendedPythonGenerator} from '../editor/extended_python_generator'; +import { createGeneratorContext, GeneratorContext } from '../editor/generator_context'; // Types, constants, and functions related to modules, regardless of where the modules are stored. @@ -151,6 +152,24 @@ export function getModuleName(modulePath: string): string { return result[2]; } +function startingBlocksToModuleContent( + module: Module, startingBlocks: {[key: string]: any}) { + // Create a headless blockly workspace. + const headlessBlocklyWorkspace = new Blockly.Workspace(); + headlessBlocklyWorkspace.options.oneBasedIndex = false; + Blockly.serialization.workspaces.load(startingBlocks, headlessBlocklyWorkspace); + + const generatorContext = createGeneratorContext(); + generatorContext.setModule(module); + + const pythonCode = extendedPythonGenerator.workspaceToCode( + headlessBlocklyWorkspace, generatorContext); + const exportedBlocks = JSON.stringify(generatorContext.getExportedBlocks()); + const blocksContent = JSON.stringify( + Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); + return makeModuleContent(module, pythonCode, exportedBlocks, blocksContent); +} + /** * Returns the module content for a new Project. */ @@ -163,16 +182,7 @@ export function newProjectContent(projectName: string): string { dateModifiedMillis: 0, }; - // Create a headless blockly workspace. - const headlessBlocklyWorkspace = new Blockly.Workspace(); - headlessBlocklyWorkspace.options.oneBasedIndex = false; - Blockly.serialization.workspaces.load(startingRobotBlocks, headlessBlocklyWorkspace); - - extendedPythonGenerator.setCurrentModule(module); - const pythonCode = extendedPythonGenerator.workspaceToCode(headlessBlocklyWorkspace); - const exportedBlocks = JSON.stringify(extendedPythonGenerator.getExportedBlocks(headlessBlocklyWorkspace)); - const blocksContent = JSON.stringify(Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); - return makeModuleContent(module, pythonCode, exportedBlocks, blocksContent); + return startingBlocksToModuleContent(module, startingRobotBlocks); } /** @@ -187,16 +197,7 @@ export function newMechanismContent(projectName: string, mechanismName: string): dateModifiedMillis: 0, }; - // Create a headless blockly workspace. - const headlessBlocklyWorkspace = new Blockly.Workspace(); - headlessBlocklyWorkspace.options.oneBasedIndex = false; - Blockly.serialization.workspaces.load(startingMechanismBlocks, headlessBlocklyWorkspace); - - extendedPythonGenerator.setCurrentModule(module); - const pythonCode = extendedPythonGenerator.workspaceToCode(headlessBlocklyWorkspace); - const exportedBlocks = JSON.stringify(extendedPythonGenerator.getExportedBlocks(headlessBlocklyWorkspace)); - const blocksContent = JSON.stringify(Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); - return makeModuleContent(module, pythonCode, exportedBlocks, blocksContent); + return startingBlocksToModuleContent(module, startingMechanismBlocks); } /** @@ -211,16 +212,7 @@ export function newOpModeContent(projectName: string, opModeName: string): strin dateModifiedMillis: 0, }; - // Create a headless blockly workspace. - const headlessBlocklyWorkspace = new Blockly.Workspace(); - headlessBlocklyWorkspace.options.oneBasedIndex = false; - Blockly.serialization.workspaces.load(startingOpModeBlocks, headlessBlocklyWorkspace); - - extendedPythonGenerator.setCurrentModule(module); - const pythonCode = extendedPythonGenerator.workspaceToCode(headlessBlocklyWorkspace); - const exportedBlocks = JSON.stringify(extendedPythonGenerator.getExportedBlocks(headlessBlocklyWorkspace)); - const blocksContent = JSON.stringify(Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); - return makeModuleContent(module, pythonCode, exportedBlocks, blocksContent); + return startingBlocksToModuleContent(module, startingOpModeBlocks); } /** @@ -471,11 +463,13 @@ export function _processUploadedModule( headlessBlocklyWorkspace.options.oneBasedIndex = false; Blockly.serialization.workspaces.load( JSON.parse(blocksContent), headlessBlocklyWorkspace); - extendedPythonGenerator.setCurrentModule(module); + + const generatorContext = createGeneratorContext(); + generatorContext.setModule(module); + const pythonCode = extendedPythonGenerator.workspaceToCode( - headlessBlocklyWorkspace); - const exportedBlocks = JSON.stringify( - extendedPythonGenerator.getExportedBlocks(headlessBlocklyWorkspace)); + headlessBlocklyWorkspace, generatorContext); + const exportedBlocks = JSON.stringify(generatorContext.getExportedBlocks()); const moduleContent = makeModuleContent( module, pythonCode, exportedBlocks, blocksContent); return [moduleName, moduleType, moduleContent];