Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -252,6 +253,7 @@ const App: React.FC = () => {
const [deleteTooltip, setDeleteTooltip] = useState('Delete');
const blocklyComponent = useRef<BlocklyComponentType | null>(null);
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = useState(0);
const generatorContext = useRef<GeneratorContext | null>(null);
const blocksEditor = useRef<editor.Editor | null>(null);
const [generatedCode, setGeneratedCode] = useState('');
const [newProjectNameModalPurpose, setNewProjectNameModalPurpose] = useState('');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);

Expand All @@ -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) => {
Expand Down
6 changes: 3 additions & 3 deletions src/blocks/mrc_class_method_def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 '';
}
11 changes: 7 additions & 4 deletions src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 = '';
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
128 changes: 52 additions & 76 deletions src/editor/extended_python_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,71 @@

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';

// Extends the python generator to collect some information about functions and
// 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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}

Expand Down
111 changes: 111 additions & 0 deletions src/editor/generator_context.ts
Original file line number Diff line number Diff line change
@@ -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 [email protected] (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;
}
}
Loading