diff --git a/backend/synthesis/synthesis.py b/backend/synthesis/synthesis.py index d5f0e53f..934b6925 100644 --- a/backend/synthesis/synthesis.py +++ b/backend/synthesis/synthesis.py @@ -6,7 +6,6 @@ from django.contrib.staticfiles.utils import get_files import json - BLOCK_DIRECTORY = 'modules' OPTIONAL_FILES = { @@ -25,90 +24,236 @@ def get_number_or_default(num, default): def syntheize_modules(data: dict, zipfile: InMemoryZip) -> Tuple[InMemoryZip, Dict[str, bool]]: '''Synthesize python code for different blocks as well as user code blocks. - Different blocks present in the project is collected. + Different blocks present in the project are collected. Parameters of each dependency block as well as constant blocks are collected. Blocks, parameters and the connections (wires) between the blocks stored in a JSON file. ''' + # Initialize an empty dictionary for tracking dependencies, blocks, parameters, synhronize_frequency, optional_files dependencies = {} blocks = {} parameters = {} synhronize_frequency = {} optional_files = {} + wire_comp = data['design']['graph']['wires'] # Retrieve all wire connections from the design graph + dep_no = {} # Dictionary to store the number of blocks of each type + + # Function to process dependencies and extract block information + def process_dependency(dep, zipfile, synhronize_frequency, optional_files, dep_no, parameters, dependencies): + # Iterate over each dependency + for key, dependency in dep.items(): + + components = dependency['design']['graph']['blocks'] # Retrieve all blocks in the dependency + wire_comp.extend(dependency['design']['graph']['wires']) # Add all wires from the dependency to the main wire list + # Iterate over each block in the dependency + for block in components: + + block_id = block['id'] + block_type = block['type'] + + # Check if the block is of type 'basic.code' + if block_type == 'basic.code': + + script = block['data']['code'] # Retrieve the script code from the block's data + script_name = dependency['package']['name'] # Retrieve the script name from the dependency package + synhronize_frequency[block_id] = get_number_or_default(block['data'].get('frequency', 30), 30) # Set the synchronization frequency, defaulting to 30 + + # Check if the script name is already in optional files + if script_name in optional_files: + optional_files[script_name] = True # Mark the script as required in optional files + + script_name += dependency['package']['version'].replace('.', '') # Append version number to the script name, removing dots + + # Increment the count for this script type or initialize it + if script_name in dep_no: + dep_no[script_name] += 1 + else: + dep_no[script_name] = 1 + + + script_name += str(dep_no[script_name]) # Add the block count to the script name + dependencies[key] = script_name # Map the key to the script name in dependencies + + + zipfile.append(f'{BLOCK_DIRECTORY}/{script_name}.py', script) # Add the script to the zipfile + blocks[block_id] = {'name': script_name, 'type': block_type} # Store block information in the blocks dictionary + + # Check if the block is of type 'basic.constant' + elif block_type == 'basic.constant': + parameters[block_id] = parameters.get(block_id, []) # Initialize parameter list for the block if not already present + + # Append block's parameter data to the parameters dictionary + parameters[block_id].append({ + 'id': block['id'], + 'name': block['data']['name'], + 'value': block['data']['value']} + ) - dep_no = {} # Dictionary to store the number of blocks of each type - - for key, dependency in data['dependencies'].items(): - - components = dependency['design']['graph']['blocks'] - - for block in components: - if block['type'] == 'basic.code': - # If code, generate python file. - script = block['data']['code'] - # The file name is stored in script_name here - script_name = dependency['package']['name'] - synhronize_frequency[key] = get_number_or_default(block['data'].get('frequency', 30), 30) - # Mark the optional files required by the current block, if needed. - if script_name in OPTIONAL_FILES: - optional_files[script_name] = True - - # The version name is simply added to the name of the file - script_name += dependency['package']['version'].replace('.', '') - - # If the block already exists, then add 1 to the number to be appended to it - if script_name in dep_no: - dep_no[script_name] += 1 - else: - dep_no[script_name] = 1 - - # Add the block number to the end of the block's name - script_name += str(dep_no[script_name]) - - dependencies[key] = script_name - - # Python file is created - zipfile.append(f'{BLOCK_DIRECTORY}/{script_name}.py', script) - - elif block['type'] == 'basic.constant': - # If constant, it is a parameter. - # Since this is a parameter for a dependency block we make the dependency type block - # as the key. This is later used to identify which parameter belongs to which dependency - # block - parameters[key] = parameters.get(key, []) - parameters[key].append({ - 'id': block['id'], - 'name': block['data']['name'], - 'value': block['data']['value']} - ) - - count = 1 + # Recursively process nested dependencies if present + if 'dependencies' in dependency and dependency['dependencies']: + process_dependency(dependency['dependencies'], zipfile, synhronize_frequency, optional_files, dep_no, parameters, dependencies) + + # Process the top-level dependencies from the data + if 'dependencies' in data: + process_dependency(data['dependencies'], zipfile, synhronize_frequency, optional_files, dep_no, parameters, dependencies) + + + count = 1 # Initialize a counter for naming blocks + + + # Iterate over the blocks in the main design graph for block in data['design']['graph']['blocks']: - block_id, block_type = block['id'], block['type'] - if block_type == 'basic.code': - # If code, generate python file. - code_name = "Code_"+str(count) - count += 1 - script = block['data']['code'] - synhronize_frequency[block_id] = get_number_or_default(block['data'].get('frequency', 30), 30) - zipfile.append(f'{BLOCK_DIRECTORY}/{code_name}.py', script) - blocks[block_id] = {'name': code_name, 'type': block_type} - elif block_type == 'basic.constant': - # If constant, it is a parameter. - # Since this is a parameter at project level, we make the key as constant block ID - parameters[block_id] = [{'name': block['data']['name'], 'value': block['data']['value']}] - else: - # TODO: Check how input and output blocks behave. - # This behaviour is for Package blocks only - blocks[block_id] = {'name': dependencies[block_type], 'type': block_type} + + if 'source' not in block and 'target' not in block: # Skip blocks that have a 'source' or 'target' property + block_id, block_type = block['id'], block['type'] + # Check if the block is of type 'basic.code' + if block_type == 'basic.code': + + code_name = "Code_" + str(count) # Generate a unique code name for the block + count += 1 # Increment the block counter + script = block['data']['code'] # Retrieve the script code from the block's data + + # Set the synchronization frequency, defaulting to 30 + synhronize_frequency[block_id] = get_number_or_default(block['data'].get('frequency', 30), 30) + zipfile.append(f'{BLOCK_DIRECTORY}/{code_name}.py', script) # Add the script to the zipfile with the generated code name + blocks[block_id] = {'name': code_name, 'type': block_type} # Store block information in the blocks dictionary + + # Check if the block is of type 'basic.constant' + elif block_type == 'basic.constant': + # Add block's parameter data to the parameters dictionary + parameters[block_id] = [{'name': block['data']['name'], 'value': block['data']['value']}] + + + valid_wires = [] # Initialize a list to store valid wire connections + + + # Iterate over all wires in the wire component list + for wire in wire_comp: + + # Retrieve source and target block IDs for the wire + source_id = wire['source']['block'] + target_id = wire['target']['block'] + + # Check if the source and target blocks are in the blocks or parameters dictionary + source_in_blocks = source_id in blocks + source_in_parameters = source_id in parameters + target_in_blocks = target_id in blocks + target_in_parameters = target_id in parameters + + # Validate wires based on presence in blocks or parameters dictionaries + if (source_in_blocks and target_in_blocks) or \ + (source_in_parameters and target_in_parameters) or \ + (source_in_blocks and target_in_parameters) or \ + (source_in_parameters and target_in_blocks): + # If valid, add the wire to the valid_wires list + valid_wires.append(wire) + else: + # Mark source and target as 'absent' if not in blocks or parameters + if source_id not in blocks and source_id not in parameters: + wire['source']['ob'] = 'absent' + if target_id not in blocks and target_id not in parameters: + wire['target']['ob'] = 'absent' + # Add the wire to the valid_wires list + valid_wires.append(wire) + + # Function to process wires and handle absent blocks + def process_wires(valid_wires): + # Initialize dictionaries to track source and target ports + wire_check_source = {} + wire_check_target = {} + + # Initialize a counter for iteration + count = 0 + # Set a flag to detect changes + changes_detected = True + # Continue processing while changes are detected + while changes_detected: + changes_detected = False - data = {'blocks': blocks, 'parameters': parameters, 'synchronize_frequency': synhronize_frequency, 'wires': data['design']['graph']['wires']} + # Iterate over valid wires in reverse order + for i in range(len(valid_wires) - 1, -1, -1): + wire = valid_wires[i] + remove_wire = False # Flag to determine if the wire should be removed + + count += 1 # Increment the counter + + # Check if the source port is 'input-out' + if wire['source']['port'] == 'input-out': + port_name = wire['source']['block'] + # Check if the target is marked 'absent' + if 'ob' in wire['target'] and wire['target']['ob'] == 'absent': + # Add target details to wire_check_source with 'input-out' port + wire_check_source[port_name] = wire['target'].copy() + wire_check_source[port_name]['port'] = port_name + else: + # Add target details to wire_check_source + wire_check_source[port_name] = wire['target'] + # Mark the wire for removal + remove_wire = True + + # Check if the target port is 'output-in' + if wire['target']['port'] == 'output-in': + port_name = wire['target']['block'] + # Check if the source is marked 'absent' + if 'ob' in wire['source'] and wire['source']['ob'] == 'absent': + # Add source details to wire_check_target with 'output-in' port + wire_check_target[port_name] = wire['source'].copy() + wire_check_target[port_name]['port'] = port_name + else: + # Add source details to wire_check_target + wire_check_target[port_name] = wire['source'] + # Mark the wire for removal + remove_wire = True + + # Remove the wire from valid_wires if marked for removal + if remove_wire: + del valid_wires[i] + changes_detected = True # Set flag to true since changes were made + + # Iterate over the valid wires to update ports + for i, wire in enumerate(valid_wires): + # Check if the source port name has 36 characters + if len(wire['source'].get('port', '')) == 36: + port_name = wire['source']['port'] + # Update source with wire_check_target details if present + if port_name in wire_check_target: + valid_wires[i]['source'] = wire_check_target[port_name] + # Update port name if 'absent' flag is present + if 'ob' in valid_wires[i]['source']: + valid_wires[i]['source']['port'] = port_name + + # Check if the target port name has 36 characters + if len(wire['target'].get('port', '')) == 36: + port_name = wire['target']['port'] + # Update target with wire_check_source details if present + if port_name in wire_check_source: + valid_wires[i]['target'] = wire_check_source[port_name] + # Update port name if 'absent' flag is present + if 'ob' in valid_wires[i]['target']: + valid_wires[i]['target']['port'] = port_name + + # Return the processed list of valid wires + return valid_wires + + # Process the wires to handle 'absent' blocks + processed_wires = process_wires(valid_wires) + + # Create a final data dictionary with blocks, parameters, frequencies, and wires + data = { + 'blocks': blocks, + 'parameters': parameters, + 'synchronize_frequency': synhronize_frequency, + 'wires': processed_wires + } + # Add the data dictionary as a JSON file in the zipfile zipfile.append('data.json', json.dumps(data)) + # Return the updated zipfile and optional files dictionary return zipfile, optional_files + def synthesize_executioner(zipfile: InMemoryZip, optional_files: Dict[str, bool]) -> InMemoryZip: '''Synthesize python code necessary to run the blocks. All these files are present in django static directory. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 029a7c49..445c6d42 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "closest": "0.0.1", "dagre": "^0.8.5", "lodash": "^4.17.21", + "lodash.clonedeep": "^4.5.0", "pathfinding": "^0.4.18", "paths-js": "^0.4.11", "react": "^17.0.2", @@ -36,6 +37,9 @@ "sass": "^1.50.0", "typescript": "^4.1.2", "web-vitals": "^1.0.1" + }, + "devDependencies": { + "@types/lodash.clonedeep": "^4.5.9" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4694,6 +4698,21 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -14208,6 +14227,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14513,12 +14537,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/monaco-editor": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.41.0.tgz", - "integrity": "sha512-1o4olnZJsiLmv5pwLEAmzHTE/5geLKQ07BrGxlF4Ri/AXAc2yyDGZwHjiTqD8D/ROKUZmwMA28A+yEowLNOEcA==", - "peer": true - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 47c48c42..23dce7fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "closest": "0.0.1", "dagre": "^0.8.5", "lodash": "^4.17.21", + "lodash.clonedeep": "^4.5.0", "pathfinding": "^0.4.18", "paths-js": "^0.4.11", "react": "^17.0.2", @@ -56,5 +57,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@types/lodash.clonedeep": "^4.5.9" } } diff --git a/frontend/src/components/blocks/common/base-port/index.tsx b/frontend/src/components/blocks/common/base-port/index.tsx index cfdc7b1c..8b74650c 100644 --- a/frontend/src/components/blocks/common/base-port/index.tsx +++ b/frontend/src/components/blocks/common/base-port/index.tsx @@ -41,7 +41,7 @@ const BasePort: React.FC = (props) => { portClass = props.isInput ? 'custom-input-port': 'custom-output-port'; break; } - + // Different classes based on alignment type. Position varies depending on this. switch (alignment) { case PortModelAlignment.RIGHT: diff --git a/frontend/src/components/blocks/common/factory.tsx b/frontend/src/components/blocks/common/factory.tsx index 2da27f43..f8aecea5 100644 --- a/frontend/src/components/blocks/common/factory.tsx +++ b/frontend/src/components/blocks/common/factory.tsx @@ -3,7 +3,7 @@ import { LinkModel, NodeModel, PortModel } from "@projectstorm/react-diagrams"; import { DefaultPortModel } from "@projectstorm/react-diagrams-defaults"; import { RightAngleLinkModel } from "@projectstorm/react-diagrams-routing"; import { PortTypes, ProjectInfo } from '../../../core/constants'; -import { ProjectDesign } from '../../../core/serialiser/interfaces'; +import { Block, Dependency, ProjectDesign, Wire } from '../../../core/serialiser/interfaces'; import createCodeDialog from '../../dialogs/code-block-dialog'; import createConstantDialog from "../../dialogs/constant-block-dialog"; import createIODialog from '../../dialogs/input-output-block-dialog'; @@ -14,6 +14,7 @@ import { OutputBlockModel } from '../basic/output/output-model'; import { getCollectionBlock } from '../collection/collection-factory'; import { PackageBlockModel } from '../package/package-model'; import { BaseInputPortModel, BaseOutputPortModel, BaseParameterPortModel, BasePortModelOptions } from './base-port/port-model'; +import cloneDeep from 'lodash.clonedeep'; /** * Port model for wires which bend at 90 degrees. Unused as of now. @@ -114,6 +115,32 @@ export const createBlock = async (name: string, blockCount: number) => { return block; } +/** + + * @param type Type of the block + * @param name + * @returns block model + */ +export const createComposedBlock = async (type: string, name: string) => { + var block; + try { + switch (type) { + case 'basic.input': + block = new InputBlockModel({name:name}); + break; + case 'basic.output': + block = new OutputBlockModel({name:name}); + break; + default: + + } + } catch (error) { + console.log(error); + } + return block; +} + + /** * Load a project as Package block * @param jsonModel object conforming to the project structure @@ -127,15 +154,63 @@ export const createBlock = async (name: string, blockCount: number) => { * @returns Package block */ export const loadPackage = (jsonModel: any) => { - const model = jsonModel.editor; - const design = jsonModel.design as ProjectDesign; - const info = jsonModel.package as ProjectInfo; + const { editor: originalEditor, design: originalDesign } = jsonModel; + + // Clone the original jsonModel to work on copies + const tempJsonModelDesign = cloneDeep(originalDesign); + const tempJsonModelEditor = cloneDeep(originalEditor); + const newIdMap: { [key: string]: string } = {}; + + // Function to generate a new UUID + const generateNewId = () => + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + + // Generate new IDs for all blocks and store the mapping + tempJsonModelDesign.graph.blocks.forEach((block: Block) => { + const newId = generateNewId(); + newIdMap[block.id] = newId; + block.id = newId; + }); + + // Helper function to update model IDs in the editor layers + const updateModelIdsInLayer = (layerIndex: number) => { + const models = tempJsonModelEditor.layers[layerIndex].models; + Object.keys(models).forEach(oldId => { + const block = models[oldId]; + const newId = newIdMap[oldId]; + + if (newId) { + block.id = newId; // Update the block's internal ID + models[newId] = block; // Add the block to a new models object with the new ID + delete models[oldId]; // Delete the old key from the models object + } + }); + }; + + // Update IDs for both layers (layer 0 and layer 1) + [0, 1].forEach(updateModelIdsInLayer); + + // Update source and target block IDs for wires + tempJsonModelDesign.graph.wires.forEach((wire: Wire) => { + const newSourceId = newIdMap[wire.source.block]; + const newTargetId = newIdMap[wire.target.block]; + + if (newSourceId) wire.source.block = newSourceId; + if (newTargetId) wire.target.block = newTargetId; + }); + + // Create the package block model with updated data return new PackageBlockModel({ - model: model, - design: design, - info: info + model: tempJsonModelEditor, + design: tempJsonModelDesign as ProjectDesign, + info: jsonModel.package as ProjectInfo, + dependencies: jsonModel.dependencies as Dependency, }); -} +}; + /** * Fixed initial position for all blocks. diff --git a/frontend/src/components/blocks/package/package-model.ts b/frontend/src/components/blocks/package/package-model.ts index bd654e99..eba65a03 100644 --- a/frontend/src/components/blocks/package/package-model.ts +++ b/frontend/src/components/blocks/package/package-model.ts @@ -1,7 +1,7 @@ import BaseModel from "../common/base-model"; import { NodeModelGenerics, PortModelAlignment } from "@projectstorm/react-diagrams"; import { BaseModelOptions, DeserializeEvent } from '@projectstorm/react-canvas-core'; -import { ProjectDesign } from "../../../core/serialiser/interfaces"; +import { Dependency, ProjectDesign } from "../../../core/serialiser/interfaces"; import { createPortModel } from "../common/factory"; import { PortTypes, ProjectInfo } from "../../../core/constants"; @@ -19,6 +19,7 @@ export interface PackageBlockModelOptions extends BaseModelOptions { design: ProjectDesign; model: any; info: ProjectInfo; + dependencies: Dependency; } /** @@ -31,6 +32,7 @@ export class PackageBlockModel extends BaseModel, Partial { + getGInputsOutput: () => [{ indexOne: number, label: string, id: string }[], { indexTwo: number, label: string, id: string }[]]; +}; + +/** + * + * @param { + * isOpen: True if modal needs to be opened. + * onResolve: Will be called to indicate success / completion. + * onReject: Will be called to indicate failure. + * } + */ +const BlockDialog = ({ isOpen, onResolve, onReject, getGInputsOutput, selectedInputIds, selectedOutputIds }: BlockDialogProps) => { + + const [inputs, outputs] = getGInputsOutput(); + const [checkedState, setCheckedState] = useState<{ [key: string]: boolean }>({}); + + useEffect(() => { + const initialCheckedState: { [key: string]: boolean } = {}; + + selectedInputIds?.forEach(id => { + initialCheckedState[id] = true; + }); + + selectedOutputIds?.forEach(id => { + initialCheckedState[id] = true; + }); + + setCheckedState(initialCheckedState); + }, [selectedInputIds, selectedOutputIds]); + + const handleSubmit = () => { + const selectedInputIds = inputs.filter(item => checkedState[item.id]).map(item => item.id); + const selectedOutputIds = outputs.filter(item => checkedState[item.id]).map(item => item.id); + onResolve({ selectedInputIds, selectedOutputIds }); + } + + const handleChange = (event: React.ChangeEvent) => { + const { name, checked } = event.target; + setCheckedState(prevState => ({ + ...prevState, + [name]: checked, + })); + }; + + return ( + + + + Edit Block + + +
+
+ Global Input + {inputs.map((item: { indexOne: number, label: string, id: string }) => ( +
+ + +
+ ))} +
+
+ Global Output + {outputs.map((item: { indexTwo: number, label: string, id: string }) => ( +
+ + +
+ ))} +
+
+ Note: Only the acceptable ports for Global are mentioned above. For more information about Global port rules, please refer to the documentation. + +
+ + + + +
+ ) +} + +const createBlockDialog = create(BlockDialog); + +export default createBlockDialog; \ No newline at end of file diff --git a/frontend/src/components/menu/index.tsx b/frontend/src/components/menu/index.tsx index 3984fa7a..7d14ab2a 100644 --- a/frontend/src/components/menu/index.tsx +++ b/frontend/src/components/menu/index.tsx @@ -82,6 +82,33 @@ function MenuBar(props: MenuBarProps) { link?.click(); } + /** + * Callback for 'Save Block' option under 'File' menu. + * @param _event Mouse click event. Unused + */ + const saveBlockAsync = async (_event: ClickEvent) => { + const completed = await editor.editBlock(); + if(completed){ + const model = editor.serialise(); + const url = textFile2DataURL(JSON.stringify(model), 'text/json'); + const link = document.getElementById('saveProjectLink'); + if (link) { + link.setAttribute('href', url); + link.setAttribute('download', editor.getName() + PROJECT_FILE_EXTENSION); + link.click(); + editor.retriveCircuit(); + } else { + console.error('Save project link element not found.'); + } + } + }; + + const saveBlock = (_event: ClickEvent) => { + saveBlockAsync(_event).catch(error => { + console.error('Error saving block:', error); + }); + }; + /** * Callback when file is uploaded. * @param event File field change event. @@ -197,6 +224,7 @@ function MenuBar(props: MenuBarProps) { New File Open Save as.. + Save Block Add as block Build and Download diff --git a/frontend/src/core/constants.ts b/frontend/src/core/constants.ts index 670c3c4a..6b66eec1 100644 --- a/frontend/src/core/constants.ts +++ b/frontend/src/core/constants.ts @@ -20,4 +20,12 @@ export interface ProjectInfo { description: string; author: string; image: string; +} + +/** + * Interface for block information. + */ +export interface BlockData { + selectedInputIds: string[]; + selectedOutputIds: string[]; } \ No newline at end of file diff --git a/frontend/src/core/editor.ts b/frontend/src/core/editor.ts index 7f12b311..1789c5e0 100644 --- a/frontend/src/core/editor.ts +++ b/frontend/src/core/editor.ts @@ -1,25 +1,38 @@ import { DeleteItemsAction } from "@projectstorm/react-canvas-core"; -import createEngine, { DiagramEngine, DiagramModel, NodeModel, RightAngleLinkFactory } from "@projectstorm/react-diagrams"; +import createEngine, {DefaultLinkModel, DiagramEngine, DiagramModel, NodeModel, RightAngleLinkFactory } from "@projectstorm/react-diagrams"; import { CodeBlockFactory } from "../components/blocks/basic/code/code-factory"; import { ConstantBlockFactory } from "../components/blocks/basic/constant/constant-factory"; import { InputBlockFactory } from "../components/blocks/basic/input/input-factory"; import { OutputBlockFactory } from "../components/blocks/basic/output/output-factory"; import { BaseInputPortFactory, BaseOutputPortFactory, BaseParameterPortFactory } from "../components/blocks/common/base-port/port-factory"; -import { createBlock, editBlock, getInitialPosition, loadPackage } from "../components/blocks/common/factory"; +import { createBlock, editBlock, getInitialPosition, loadPackage,createComposedBlock } from "../components/blocks/common/factory"; import { PackageBlockFactory } from "../components/blocks/package/package-factory"; import { PackageBlockModel } from "../components/blocks/package/package-model"; import createProjectInfoDialog from "../components/dialogs/project-info-dialog"; -import { ProjectInfo } from "./constants"; +import { ProjectInfo,BlockData } from "./constants"; import { convertToOld } from "./serialiser/converter"; +import BaseModel from "../components/blocks/common/base-model"; +import { count } from "console"; +import createBlockDialog from "../components/dialogs/blocks-dialog"; +import cloneDeep from 'lodash.clonedeep'; +import { PortModelOptions } from '@projectstorm/react-diagrams-core'; +declare module '@projectstorm/react-diagrams-core' { + export interface PortModelOptions { + label?: string; + } +} class Editor { private static instance: Editor; private currentProjectName: string; private projectInfo: ProjectInfo; + private BlockData: BlockData; - private stack: {model: DiagramModel, info: ProjectInfo, node: PackageBlockModel}[]; + + private stack: { model: DiagramModel, info: ProjectInfo, node: PackageBlockModel }[]; + private stackOfBlock: { model: DiagramModel, info: ProjectInfo}[]; private activeModel: DiagramModel; private blockCount: number = 0; @@ -35,6 +48,7 @@ class Editor { this.activeModel = new DiagramModel(); // Use an array as stack, to keep track of levels of circuit model. this.stack = []; + this.stackOfBlock = []; this.engine.setModel(this.activeModel); this.registerFactories(); this.projectInfo = { @@ -44,6 +58,10 @@ class Editor { 'author': '', 'image': '' }; + this.BlockData = { + 'selectedInputIds': [], + 'selectedOutputIds': [] + }; } /** @@ -64,7 +82,6 @@ class Editor { // register an DeleteItemsAction with custom keyCodes (in this case, only Delete key) this.engine.getActionEventBus().registerAction(new DeleteItemsAction({ keyCodes: [46] })); } - /** * Main entry point to get Editor object, since constructor is private. * @returns instance of Editor object @@ -123,6 +140,116 @@ class Editor { return { editor : this.activeModel.serialize(), ...data}; } + + public async processBlock(model: DiagramModel, data: BlockData): Promise { + // Process selected input IDs + await Promise.all(data.selectedInputIds.map(async (id: string) => { + const [blockid, name, linkID] = id.split(":"); + await this.addComposedBlock('basic.input', name,blockid); + + })); + + // Process selected output IDs + await Promise.all(data.selectedOutputIds.map(async (id: string) => { + const [blockid, name, linkID] = id.split(":"); + await this.addComposedBlock('basic.output', name,blockid); + + })); + } + + + + + public getGInputsOutput(): [{ indexOne: number, label: string, id:string }[], { indexTwo: number, label: string,id:string }[]] { + let indexOne = 0; + let indexTwo = 0; + let valueOne: { indexOne: number, label: string, id:string }[] = []; + let valueTwo: { indexTwo: number, label: string, id:string }[] = []; + this.activeModel.getNodes().forEach((node) => { + + if (node instanceof BaseModel) { + var options = node.getOptions(); + var dataPorts = node.getPorts(); + Object.keys(dataPorts).forEach(portName => { + const port = dataPorts[portName]; + const portOptions = port.getOptions(); + const links = port.getLinks(); + var linkIds = Object.keys(links); + if(portOptions.type == 'port.input'){ + if(linkIds.length == 0){ + indexOne++; + let label = ``; + var id = `${options.id}:${portOptions.label}:${linkIds}`; + if(node.getType() == 'block.package'){ + label = `${node.getType()} -> : ${options.info.name} : ${portOptions.label}`; + }else{ + label = `${node.getType()} -> : ${portOptions.label}`; + } + valueOne.push({ indexOne, label, id }); + } + + }else if(portOptions.type == 'port.output'){ + indexTwo++; + let label = ``; + var id = `${options.id}:${portOptions.label}:${linkIds}`; + if(node.getType() == 'block.package'){ + label = `${node.getType()} -> : ${options.info.name} : ${portOptions.label}`; + }else{ + label = `${node.getType()} -> : ${portOptions.label}`; + } + valueTwo.push({ indexTwo, label, id }); + } + + }); + } + + }) + return [valueOne, valueTwo]; + } + + /** + * Callback for the 'Edit Block' button in menu. + * Opens a dialog box and saves the data entered to Block variable. + */ + public async editBlock(): Promise { + try { + + // Create deep copies using lodash.cloneDeep + const activeModelCopy = cloneDeep(this.activeModel); + const projectInfoCopy = cloneDeep(this.projectInfo); + + // Push the deep copies onto the stack + this.stackOfBlock.push({ model: activeModelCopy, info: projectInfoCopy }); + + // Open the block dialog and await the result + const data = await createBlockDialog({ isOpen: true, getGInputsOutput: this.getGInputsOutput.bind(this) }); + this.BlockData = data; + + // Process the block data + await this.processBlock(this.activeModel, data); + console.log("Block editing completed successfully."); + return true; + + } catch (error) { + console.error('Error in editBlock:', error); + console.log('Block dialog closed'); + return false; + } + } + + public retriveCircuit(){ + if (this.stackOfBlock.length) { + + const {model, info} = this.stackOfBlock.pop()!; + this.activeModel = model; + this.projectInfo = info; + this.engine.setModel(this.activeModel); + this.engine.repaintCanvas(); + + } + } + + /** * Getter for Project Name * @returns Project name @@ -151,6 +278,74 @@ class Editor { } } + public nullLinkNodes(type:string, name: string,blockID:string){ + // Get all nodes from the model + const nodes = this.activeModel.getNodes(); + + // Iterate over each node + for (const node of Object.values(nodes)) { + // Get all ports from the current node + const ports = node.getPorts(); + + // Iterate over each port to check if the name matches the given port name + for (const port of Object.values(ports)) { + if (port.getOptions().label === name && node.getID() === blockID) { + const link = new DefaultLinkModel(); + if(port.getType()== 'port.input'){ + link.setTargetPort(port); + this.activeModel.addLink(link); + return link.getID(); + }else if(port.getType()== 'port.output'){ + link.setSourcePort(port); + this.activeModel.addLink(link); + return link.getID(); + } + + } + } + } + + // Return null if no matching port is found + return ""; + } + + + /** + * Add the given type of block for composed. + * @param name : Name / type of the block to add to model. + */ + public async addComposedBlock(type: string,name: string,blockID: string): Promise { + this.blockCount += 1; + const linkID = this.nullLinkNodes(type, name,blockID); + const block = await createComposedBlock(type,name); + if (block) { + + block.setPosition(...getInitialPosition()) + this.activeModel.addNode(block); + this.engine.repaintCanvas(); + + const link = this.activeModel.getLink(linkID); + if (!link) { + console.error('Link not found'); + return; + } + + const newPort = block.getPort(); + if (!newPort) { + console.error('New source port not found'); + return; + } + + if(type == "basic.input"){ + link.setSourcePort(newPort); + }else if(type == "basic.output"){ + link.setTargetPort(newPort); + } + + this.engine.repaintCanvas(); + } + } + /** * Callback for the 'Edit Project Information' button in menu. * Opens a dialog box and saves the data entered to projectInfo variable. diff --git a/frontend/src/core/serialiser/converter.ts b/frontend/src/core/serialiser/converter.ts index d84c84f3..3b6858ef 100644 --- a/frontend/src/core/serialiser/converter.ts +++ b/frontend/src/core/serialiser/converter.ts @@ -46,9 +46,6 @@ function getWires(model: DiagramModel): Wire[] { const port2 = link.getTargetPort(); if (port1 instanceof BasePortModel && port2 instanceof BasePortModel) { - // Source should correspond to the block which is giving the output - // Target should correspond to the block receiving the input. - // So source is the port of type Output and target is the port of type Input or Parameter const source = port1.getType() === PortTypes.OUTPUT ? port1 : port2; const target = port1.getType() !== PortTypes.OUTPUT ? port1 : port2; wires.push( @@ -80,11 +77,10 @@ function getWires(model: DiagramModel): Wire[] { function getBlocksAndDependencies(model: DiagramModel) { const blocks: Block[] = []; const dependencies: { [k: string]: Dependency } = {}; - // Iterate over all the nodes and separate them into normal blocks and dependency blocks - model.getNodes().forEach((node) => { + model.getNodes().forEach((node) => { if (node instanceof BaseModel) { - const block = { + const block: Block = { id: node.getID(), type: node.getType(), data: node.getData(), @@ -92,22 +88,23 @@ function getBlocksAndDependencies(model: DiagramModel) { x: node.getPosition().x, y: node.getPosition().y } - } - // If a node is of Package type then its included in Dependencies + }; + if (node instanceof PackageBlockModel) { - // The type is changed to a random string because a single package can be used multiple times - // with its own parameters and data. So making it a unique ID prevents any interference of data. - block.type = makeid(40); - // Add the design and package info of the package blocks under dependencies + const packageBlockModel = node as PackageBlockModel; + block.type = makeid(40); // Generate a unique identifier for the dependency + + // Add the package information, design, and nested dependencies to the dependencies object dependencies[block.type] = { package: node.info, - design: node.design + design: node.design, + dependencies: node.dependencies } - } + blocks.push(block); } - }) - + }); + // Return the blocks and dependencies as an object return { blocks: blocks, dependencies: dependencies }; -} \ No newline at end of file +} diff --git a/frontend/src/core/serialiser/interfaces.ts b/frontend/src/core/serialiser/interfaces.ts index b1bc1188..c516b521 100644 --- a/frontend/src/core/serialiser/interfaces.ts +++ b/frontend/src/core/serialiser/interfaces.ts @@ -67,6 +67,7 @@ export interface ProjectDesign { export interface Dependency { // Meta info about project (model) package: ProjectInfo; - // Data about blocks and connections. + // Data about blocks, connections and nested dependency. design: ProjectDesign; + dependencies: Dependency; } \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6a682fd8..ed1381ff 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -10063,4 +10063,4 @@ yargs@^16.2.0: yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== \ No newline at end of file