diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx
index d5543848..c51922bc 100644
--- a/src/reactComponents/Menu.tsx
+++ b/src/reactComponents/Menu.tsx
@@ -21,6 +21,7 @@
import * as Antd from 'antd';
import * as React from 'react';
import * as commonStorage from '../storage/common_storage';
+import * as createPythonFiles from '../storage/create_python_files';
import * as I18Next from 'react-i18next';
import {TabType } from '../types/TabType';
@@ -128,7 +129,7 @@ function getMenuItems(t: (key: string) => string, project: commonStorage.Project
return [
getItem(t('PROJECT'), 'project', , [
getItem(t('SAVE'), 'save', ),
- getItem(t('DEPLOY'), 'deploy', undefined, undefined, true),
+ getItem(t('DEPLOY'), 'deploy'),
getDivider(),
getItem(t('MANAGE') + '...', 'manageProjects'),
]),
@@ -280,6 +281,12 @@ export function Component(props: MenuProps): React.JSX.Element {
props.openWPIToolboxSettings();
} else if (key === 'theme') {
setThemeModalOpen(true);
+ } else if (key == 'deploy') {
+ if (props.project && props.storage) {
+ handleDeploy();
+ } else {
+ props.setAlertErrorMessage(t('NO_PROJECT_SELECTED'));
+ }
} else if (key.startsWith('setlang:')) {
const lang = key.split(':')[1];
i18n.changeLanguage(lang);
@@ -290,6 +297,31 @@ export function Component(props: MenuProps): React.JSX.Element {
}
};
+ /** Handles the deploy action to generate and download Python files. */
+ const handleDeploy = async (): Promise => {
+ if (!props.project || !props.storage) {
+ return;
+ }
+
+ try {
+ const blobUrl = await createPythonFiles.producePythonProjectBlob(props.project, props.storage);
+
+ // Create a temporary link to download the file
+ const link = document.createElement('a');
+ link.href = blobUrl;
+ link.download = `${props.project.projectName}.zip`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // Clean up the blob URL
+ URL.revokeObjectURL(blobUrl);
+ } catch (error) {
+ console.error('Failed to deploy project:', error);
+ props.setAlertErrorMessage(t('DEPLOY_FAILED') || 'Failed to deploy project');
+ }
+ };
+
/** Handles closing the file management modal. */
const handleFileModalClose = (): void => {
console.log('Modal onCancel called');
diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts
index ea35f19f..ebcc28aa 100644
--- a/src/storage/client_side_storage.ts
+++ b/src/storage/client_side_storage.ts
@@ -154,8 +154,8 @@ class ClientSideStorage implements commonStorage.Storage {
...module,
};
const project: commonStorage.Project = {
- projectName: moduleName,
- userVisibleName: commonStorage.snakeCaseToPascalCase(moduleName),
+ projectName: module.projectName,
+ userVisibleName: commonStorage.snakeCaseToPascalCase(module.projectName),
robot: robot,
mechanisms: [],
opModes: [],
@@ -242,7 +242,7 @@ class ClientSideStorage implements commonStorage.Storage {
async createProject(projectName: string, robotContent: string, opmodeContent : string): Promise {
const modulePath = commonStorage.makeRobotPath(projectName);
- const opmodePath = commonStorage.makeModulePath(projectName, 'Teleop');
+ const opmodePath = commonStorage.makeModulePath(projectName, 'teleop');
await this._saveModule(commonStorage.MODULE_TYPE_ROBOT, modulePath, robotContent);
await this._saveModule(commonStorage.MODULE_TYPE_OPMODE, opmodePath, opmodeContent);
@@ -580,7 +580,7 @@ class ClientSideStorage implements commonStorage.Storage {
let moduleNameToContentText: {[key: string]: string}; // key is module name, value is module content
try {
[moduleNameToType, moduleNameToContentText] = await commonStorage.processUploadedBlob(
- projectName, blobUrl);
+ blobUrl);
} catch (e) {
console.log('commonStorage.processUploadedBlob failed.');
reject(new Error('commonStorage.processUploadedBlob failed.'));
diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts
index cbd5eff0..536f87a4 100644
--- a/src/storage/common_storage.ts
+++ b/src/storage/common_storage.ts
@@ -454,7 +454,7 @@ export function makeModulePath(projectName: string, moduleName: string): string
* Returns the robot module path for the given project names.
*/
export function makeRobotPath(projectName: string): string {
- return makeModulePath(projectName, projectName);
+ return makeModulePath(projectName, 'robot');
}
/**
@@ -502,7 +502,7 @@ export function newRobotContent(projectName: string): string {
modulePath: makeRobotPath(projectName),
moduleType: MODULE_TYPE_ROBOT,
projectName: projectName,
- moduleName: projectName,
+ moduleName: 'robot',
dateModifiedMillis: 0,
className: CLASS_NAME_ROBOT,
};
@@ -654,7 +654,7 @@ export function makeUploadProjectName(
* Process the uploaded blob to get the module types and contents.
*/
export async function processUploadedBlob(
- projectName: string, blobUrl: string)
+ blobUrl: string)
: Promise<[{ [key: string]: string }, { [key: string]: string }]> {
const prefix = 'data:application/octet-stream;base64,';
@@ -683,7 +683,7 @@ export async function processUploadedBlob(
for (const filename in files) {
const uploadedContent = files[filename];
const [moduleName, moduleType, moduleContent] = _processUploadedModule(
- projectName, filename, uploadedContent);
+ filename, uploadedContent);
moduleNameToType[moduleName] = moduleType;
moduleNameToContentText[moduleName] = moduleContent;
}
@@ -695,12 +695,12 @@ export async function processUploadedBlob(
* Processes an uploaded module to get the module name, type, and content text.
*/
export function _processUploadedModule(
- projectName: string, filename: string, uploadedContent: string)
+ filename: string, uploadedContent: string)
: [string, string, string] {
const moduleContent = parseModuleContentText(uploadedContent);
const moduleType = moduleContent.getModuleType();
- const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? projectName : filename;
+ const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? 'robot' : filename;
const moduleContentText = moduleContent.getModuleContentText();
return [moduleName, moduleType, moduleContentText];
}
diff --git a/src/storage/create_python_files.ts b/src/storage/create_python_files.ts
new file mode 100644
index 00000000..d815c2bc
--- /dev/null
+++ b/src/storage/create_python_files.ts
@@ -0,0 +1,196 @@
+/**
+ * @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.
+ */
+
+/**
+ * @fileoverview Functions for generating Python code from Blockly projects.
+ * This module uses headless Blockly to convert block-based projects into
+ * Python code files and provides utilities for packaging them into downloadable zip files.
+ */
+
+import * as Blockly from 'blockly/core';
+import { extendedPythonGenerator } from '../editor/extended_python_generator';
+import { Project, Module, Storage, parseModuleContentText } from './common_storage';
+import JSZip from 'jszip';
+import { GeneratorContext } from '../editor/generator_context';
+
+/** Result of Python code generation for a single module */
+export interface ModulePythonResult {
+ moduleName: string;
+ pythonCode: string;
+ success: boolean;
+ error?: string;
+}
+
+/** Result of Python code generation for an entire project */
+export interface ProjectPythonResult {
+ projectName: string;
+ modules: ModulePythonResult[];
+ success: boolean;
+ errorCount: number;
+}
+
+/**
+ * Generate Python code for a single module using headless Blockly
+ * @param module The module containing Blockly JSON
+ * @param storage The storage interface to fetch module content
+ * @returns Result containing generated Python code or error
+ */
+export async function generatePythonForModule(module: Module, storage: Storage): Promise {
+ try {
+ // Fetch the module content from storage
+ const moduleContentText = await storage.fetchModuleContentText(module.modulePath);
+ const moduleContent = parseModuleContentText(moduleContentText);
+
+ // Create a headless workspace
+ const workspace = new Blockly.Workspace();
+
+ // Parse and load the JSON into the workspace
+ const blocks = moduleContent.getBlocks();
+
+ Blockly.serialization.workspaces.load(blocks, workspace);
+
+
+ // Create and set up generator context like the editor does
+ const generatorContext = new GeneratorContext();
+ generatorContext.setModule(module);
+
+ // Initialize the generator if not already done
+ if (!extendedPythonGenerator.isInitialized) {
+ extendedPythonGenerator.init(workspace);
+ }
+
+ // Generate Python code using the same method as the editor
+ const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode(workspace, generatorContext);
+
+ // Clean up the workspace
+ workspace.dispose();
+
+ return {
+ moduleName: module.moduleName,
+ pythonCode,
+ success: true,
+ };
+ } catch (error) {
+ console.error('Error generating Python for module', module.moduleName, ':', error);
+ return {
+ moduleName: module.moduleName,
+ pythonCode: '',
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
+
+/**
+ * Generate Python code for all modules in a project using headless Blockly
+ * @param project The project containing multiple modules
+ * @param storage The storage interface to fetch module content
+ * @returns Result containing Python code for all modules
+ */
+export async function generatePythonForProject(project: Project, storage: Storage): Promise {
+ const moduleResults: ModulePythonResult[] = [];
+ let errorCount = 0;
+
+ // Process the robot module
+ const robotResult = await generatePythonForModule(project.robot, storage);
+ moduleResults.push(robotResult);
+ if (!robotResult.success) {
+ errorCount++;
+ }
+
+ // Process all mechanism modules
+ for (const mechanism of project.mechanisms) {
+ const result = await generatePythonForModule(mechanism, storage);
+ moduleResults.push(result);
+ if (!result.success) {
+ errorCount++;
+ }
+ }
+
+ // Process all opmode modules
+ for (const opMode of project.opModes) {
+ const result = await generatePythonForModule(opMode, storage);
+ moduleResults.push(result);
+ if (!result.success) {
+ errorCount++;
+ }
+ }
+
+ return {
+ projectName: project.projectName,
+ modules: moduleResults,
+ success: errorCount === 0,
+ errorCount,
+ };
+}
+
+/**
+ * Generate Python files content as a map for easy file creation
+ * @param project The project containing multiple modules
+ * @param storage The storage interface to fetch module content
+ * @returns Map of filename to Python code content
+ */
+export async function generatePythonFilesMap(project: Project, storage: Storage): Promise