Skip to content

Commit cdf6d4a

Browse files
authored
Add new mechanism to robot (#422)
* Add new mechanism to robot * Fix to match automatically adding to robot, and to change my_arm to myArm * Respond to review comments * Fix typo * Make it so that all places that need updating of the robot editor update it
1 parent 296840a commit cdf6d4a

8 files changed

Lines changed: 197 additions & 63 deletions

File tree

docs/testing_before_pr_checklist.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ For each PR, make sure each of these works
66
* [ ] Add a mechanism "Arm" from the + next to Tabs on the Robot and Mechanism arm
77
# Using robot
88
* [ ] Add a component to the Robot
9-
* [ ] Add a mechanism to the Robot
9+
* [ ] Make sure that the Arm shows up as myArm in the robot
1010
* [ ] When editing an OpMode, make sure the component shows up in the toolbox Robot -> Components ->
11-
* [ ] When editing an OpMode, make sure the mechanism shows up in the toolbox Robot -> Mechanisms -> my_arm
11+
* [ ] When editing an OpMode, make sure the mechanism shows up in the toolbox Robot -> Mechanisms -> myArm
1212
# Events
1313
* [ ] Add an event to the Mechanism
14-
* [ ] In Robot, make sure you can see the event handler in the toolbox Robot -> Mechanisms -> my_arm -> Events
15-
* [ ] In Opmode, make sure you can see the event handler in the toolbox Robot -> Mechanisms -> my_arm -> Events
14+
* [ ] In Robot, make sure you can see the event handler in the toolbox Robot -> Mechanisms -> myArm -> Events
15+
* [ ] In Opmode, make sure you can see the event handler in the toolbox Robot -> Mechanisms -> myArm -> Events
1616
# Mechanisms
1717
* [ ] Add a public component to the mechanism
1818
* [ ] Add a private component to the mechanism

frontend/blocks/mrc_mechanism.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ const MECHANISM = {
9595
*/
9696
init: function (this: MechanismBlock): void {
9797
this.mrcHasNotInHolderWarning = false;
98+
this.mrcMechanismModuleId = '';
99+
this.mrcMechanismId = this.id;
100+
this.mrcImportModule = '';
101+
this.mrcParameters = [];
98102
this.setStyle(MRC_STYLE_MECHANISMS);
99103
const nameField = new Blockly.FieldTextInput('')
100104
nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField));

frontend/blocks/mrc_mechanism_component_holder.ts

Lines changed: 94 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2828
import * as storageModule from '../storage/module';
2929
import * as storageModuleContent from '../storage/module_content';
3030
import { NONCOPYABLE_BLOCK } from './noncopyable_block';
31-
import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism';
31+
import { createMechanismBlock, BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism';
3232
import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism';
3333
import { MechanismBlock } from './mrc_mechanism';
34-
import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component';
34+
import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component';
3535
import { OUTPUT_NAME as COMPONENT_OUTPUT } from './mrc_component';
3636
import { ComponentBlock } from './mrc_component';
37-
import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event';
37+
import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event';
3838
import { OUTPUT_NAME as EVENT_OUTPUT } from './mrc_event';
3939
import { EventBlock } from './mrc_event';
4040
import { getModuleTypeForWorkspace } from './utils/workspaces';
@@ -103,49 +103,49 @@ const MECHANISM_COMPONENT_HOLDER = {
103103
// Handle mechanisms input visibility
104104
if (!this.mrcHideMechanisms) {
105105
this.appendStatementInput(INPUT_MECHANISMS)
106-
.setCheck(MECHANISM_OUTPUT)
107-
.appendField(Blockly.Msg.MECHANISMS);
106+
.setCheck(MECHANISM_OUTPUT)
107+
.appendField(Blockly.Msg.MECHANISMS);
108108
}
109109

110110
const componentsField = new Blockly.FieldLabel(Blockly.Msg.COMPONENTS);
111111
this.appendStatementInput(INPUT_COMPONENTS)
112-
.setCheck(COMPONENT_OUTPUT)
113-
.appendField(componentsField);
112+
.setCheck(COMPONENT_OUTPUT)
113+
.appendField(componentsField);
114114

115115
// Handle private components input visibility
116116
if (!this.mrcHidePrivateComponents) {
117-
const privateComponentsField = new Blockly.FieldLabel(Blockly.Msg.PRIVATE_COMPONENTS);
118-
this.appendStatementInput(INPUT_PRIVATE_COMPONENTS)
119-
.setCheck(COMPONENT_OUTPUT)
120-
.appendField(privateComponentsField);
121-
// Set tooltips on both componentsField and privateComponentsField.
122-
componentsField.setTooltip(Blockly.Msg.COMPONENTS_TOOLTIP);
123-
privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP);
117+
const privateComponentsField = new Blockly.FieldLabel(Blockly.Msg.PRIVATE_COMPONENTS);
118+
this.appendStatementInput(INPUT_PRIVATE_COMPONENTS)
119+
.setCheck(COMPONENT_OUTPUT)
120+
.appendField(privateComponentsField);
121+
// Set tooltips on both componentsField and privateComponentsField.
122+
componentsField.setTooltip(Blockly.Msg.COMPONENTS_TOOLTIP);
123+
privateComponentsField.setTooltip(Blockly.Msg.PRIVATE_COMPONENTS_TOOLTIP);
124124
}
125125

126126
this.appendStatementInput(INPUT_EVENTS)
127-
.setCheck(EVENT_OUTPUT)
128-
.appendField(Blockly.Msg.EVENTS);
127+
.setCheck(EVENT_OUTPUT)
128+
.appendField(Blockly.Msg.EVENTS);
129129
},
130130
/**
131131
* mrcOnLoad is called for each MechanismComponentHolderBlock when the blocks are loaded in the blockly
132132
* workspace.
133133
*/
134-
mrcOnLoad: function(this: MechanismComponentHolderBlock, editor: Editor): void {
134+
mrcOnLoad: function (this: MechanismComponentHolderBlock, editor: Editor): void {
135135
this.collectDescendants(editor, false);
136136
},
137137
/**
138138
* mrcOnDescendantDisconnect is called for each MechanismComponentHolderBlock when any descendant is
139139
* disconnected.
140140
*/
141-
mrcOnDescendantDisconnect: function(this: MechanismComponentHolderBlock, editor: Editor): void {
141+
mrcOnDescendantDisconnect: function (this: MechanismComponentHolderBlock, editor: Editor): void {
142142
this.collectDescendants(editor, true);
143143
},
144144
mrcDescendantsMayHaveChanged: function (this: MechanismComponentHolderBlock, editor: Editor): void {
145145
this.collectDescendants(editor, true);
146146
},
147147
collectDescendants: function (
148-
this: MechanismComponentHolderBlock, editor: Editor, updateToolboxIfDescendantsChanged: boolean): void {
148+
this: MechanismComponentHolderBlock, editor: Editor, updateToolboxIfDescendantsChanged: boolean): void {
149149
let mechanismBlockIds = '';
150150
let componentBlockIds = '';
151151
let privateComponentBlockIds = '';
@@ -202,9 +202,9 @@ const MECHANISM_COMPONENT_HOLDER = {
202202

203203
if (updateToolboxIfDescendantsChanged) {
204204
if (mechanismBlockIds !== this.mrcMechanismBlockIds ||
205-
componentBlockIds !== this.mrcComponentBlockIds ||
206-
privateComponentBlockIds !== this.mrcPrivateComponentBlockIds ||
207-
eventBlockIds !== this.mrcEventBlockIds) {
205+
componentBlockIds !== this.mrcComponentBlockIds ||
206+
privateComponentBlockIds !== this.mrcPrivateComponentBlockIds ||
207+
eventBlockIds !== this.mrcEventBlockIds) {
208208
editor.updateToolboxAfterDelay();
209209
}
210210
}
@@ -221,14 +221,14 @@ const MECHANISM_COMPONENT_HOLDER = {
221221
setNameOfChildBlock(this: MechanismComponentHolderBlock, child: Blockly.Block): void {
222222
const otherNames: string[] = []
223223
this.getDescendants(true)
224-
.filter(descendant =>
225-
descendant.type === MRC_MECHANISM_NAME ||
226-
descendant.type === MRC_COMPONENT_NAME ||
227-
descendant.type === MRC_EVENT_NAME)
228-
.filter(descendant => descendant.id !== child.id)
229-
.forEach(descendant => {
230-
otherNames.push(descendant.getFieldValue('NAME'));
231-
});
224+
.filter(descendant =>
225+
descendant.type === MRC_MECHANISM_NAME ||
226+
descendant.type === MRC_COMPONENT_NAME ||
227+
descendant.type === MRC_EVENT_NAME)
228+
.filter(descendant => descendant.id !== child.id)
229+
.forEach(descendant => {
230+
otherNames.push(descendant.getFieldValue('NAME'));
231+
});
232232
const currentName = child.getFieldValue('NAME');
233233
// mechanism and component names must be valid python identifiers.
234234
// event names do not need to be valid python identifiers.
@@ -367,8 +367,8 @@ function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, genera
367367
}
368368

369369
export const pythonFromBlock = function (
370-
block: MechanismComponentHolderBlock,
371-
generator: ExtendedPythonGenerator) {
370+
block: MechanismComponentHolderBlock,
371+
generator: ExtendedPythonGenerator) {
372372
switch (generator.getModuleType()) {
373373
case storageModule.ModuleType.ROBOT:
374374
pythonFromBlockInRobot(block, generator);
@@ -423,7 +423,7 @@ export function hasAnyComponents(workspace: Blockly.Workspace): boolean {
423423
* Collects the args for the mechanism's constructor and defineHardware method.
424424
*/
425425
export function getMechanismInitArgNames(workspace: Blockly.Workspace, mechanismInitArgNames: string[]): void {
426-
workspace.getBlocksByType(BLOCK_NAME).forEach( block => {
426+
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
427427
const inputConnections: Blockly.Connection[] = [];
428428
const componentsInput = block.getInput(INPUT_COMPONENTS);
429429
if (componentsInput && componentsInput.connection) {
@@ -449,8 +449,8 @@ export function getMechanismInitArgNames(workspace: Blockly.Workspace, mechanism
449449
}
450450

451451
export function getMechanisms(
452-
workspace: Blockly.Workspace,
453-
mechanisms: storageModuleContent.MechanismInRobot[]): void {
452+
workspace: Blockly.Workspace,
453+
mechanisms: storageModuleContent.MechanismInRobot[]): void {
454454
// Get the holder block and ask it for the mechanisms.
455455
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
456456
const mechanismsFromHolder: storageModuleContent.MechanismInRobot[] =
@@ -460,8 +460,8 @@ export function getMechanisms(
460460
}
461461

462462
export function getComponents(
463-
workspace: Blockly.Workspace,
464-
components: storageModuleContent.Component[]): void {
463+
workspace: Blockly.Workspace,
464+
components: storageModuleContent.Component[]): void {
465465
// Get the holder block and ask it for the components.
466466
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
467467
const componentsFromHolder: storageModuleContent.Component[] =
@@ -471,8 +471,8 @@ export function getComponents(
471471
}
472472

473473
export function getPrivateComponents(
474-
workspace: Blockly.Workspace,
475-
components: storageModuleContent.Component[]): void {
474+
workspace: Blockly.Workspace,
475+
components: storageModuleContent.Component[]): void {
476476
// Get the holder block and ask it for the private components.
477477
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
478478
const privateComponentsFromHolder: storageModuleContent.Component[] =
@@ -482,16 +482,16 @@ export function getPrivateComponents(
482482
}
483483

484484
export function getAllComponents(
485-
workspace: Blockly.Workspace,
486-
components: storageModuleContent.Component[]): void {
485+
workspace: Blockly.Workspace,
486+
components: storageModuleContent.Component[]): void {
487487
// Get both regular and private components for when creating a mechanism
488488
getComponents(workspace, components);
489489
getPrivateComponents(workspace, components);
490490
}
491491

492492
export function getEvents(
493-
workspace: Blockly.Workspace,
494-
events: storageModuleContent.Event[]): void {
493+
workspace: Blockly.Workspace,
494+
events: storageModuleContent.Event[]): void {
495495
// Get the holder block and ask it for the events.
496496
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
497497
const eventsFromHolder: storageModuleContent.Event[] =
@@ -507,17 +507,61 @@ export function mrcDescendantsMayHaveChanged(workspace: Blockly.Workspace, edito
507507
});
508508
}
509509

510+
// This is called to add a mechanism block to the robot's workspace when a new mechanism is created
511+
// since we aren't guaranteed that the robot has a live workspace at this time, we have to work in JSON.
512+
export function mrcAddMechanismBlockToRobotContent(robotContent: storageModuleContent.ModuleContent,
513+
mechanism: storageModule.Mechanism): void {
514+
const mechanismBlock = createMechanismBlock(mechanism, [], false);
515+
516+
// Build the plain JSON object for the new mechanism block (Blockly serialization format).
517+
const mechanismBlockJson: {[key: string]: unknown} = { type: mechanismBlock.type };
518+
if (mechanismBlock.extraState) {
519+
mechanismBlockJson['extraState'] = mechanismBlock.extraState;
520+
}
521+
if (mechanismBlock.fields) {
522+
mechanismBlockJson['fields'] = mechanismBlock.fields;
523+
}
524+
if (mechanismBlock.inputs) {
525+
mechanismBlockJson['inputs'] = mechanismBlock.inputs;
526+
}
527+
528+
// Mutate the JSON blocks directly (no live workspace needed).
529+
const blocks = robotContent.getBlocks();
530+
const topBlocks: {[key: string]: any}[] = blocks?.blocks?.blocks ?? [];
531+
532+
for (const block of topBlocks) {
533+
if (block.type !== BLOCK_NAME) {
534+
continue;
535+
}
536+
if (!block.inputs) {
537+
block.inputs = {};
538+
}
539+
if (!block.inputs[INPUT_MECHANISMS]) {
540+
block.inputs[INPUT_MECHANISMS] = { block: mechanismBlockJson };
541+
} else {
542+
// Walk to the last block in the chain and append.
543+
let lastBlock = block.inputs[INPUT_MECHANISMS].block;
544+
while (lastBlock.next?.block) {
545+
lastBlock = lastBlock.next.block;
546+
}
547+
lastBlock.next = { block: mechanismBlockJson };
548+
}
549+
}
550+
551+
robotContent.setBlocks(blocks);
552+
}
553+
510554
/**
511555
* Upgrades the MechanismComponentHolderBlock in the given workspace from version 001 to 002 by
512556
* setting mrcHidePrivateComponents to true.
513557
* This function should only be called when upgrading old projects.
514558
*/
515559
export function upgrade_001_to_002(workspace: Blockly.Workspace) {
516-
// Make sure the module type is ROBOT.
517-
if (getModuleTypeForWorkspace(workspace) !== storageModule.ModuleType.ROBOT) {
518-
throw new Error('upgrade_001_to_002 should only be called for a robot module.');
560+
// Make sure the module type is ROBOT.
561+
if (getModuleTypeForWorkspace(workspace) !== storageModule.ModuleType.ROBOT) {
562+
throw new Error('upgrade_001_to_002 should only be called for a robot module.');
563+
}
564+
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
565+
(block as MechanismComponentHolderBlock).mrcHidePrivateComponents = true;
566+
});
519567
}
520-
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
521-
(block as MechanismComponentHolderBlock).mrcHidePrivateComponents = true;
522-
});
523-
}

frontend/editor/editor.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_hol
3535
import * as workspaces from '../blocks/utils/workspaces';
3636
//import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests';
3737
import { applyExpandedCategories, getToolboxJSON } from '../toolbox/toolbox';
38+
import { mrcAddMechanismBlockToRobotContent } from '../blocks/mrc_mechanism_component_holder';
3839

3940
const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = {
4041
kind: 'categoryToolbox',
@@ -716,6 +717,48 @@ export class Editor {
716717
return workspace ? Editor.getEditorForBlocklyWorkspace(workspace) : null;
717718
}
718719

720+
public static getEditorForModulePath(modulePath: string): Editor | undefined {
721+
return Editor.modulePathToEditor[modulePath];
722+
}
723+
724+
/**
725+
* Adds a newly-created mechanism to this robot editor's live workspace by serializing
726+
* the current workspace state, injecting the mechanism block JSON, and reloading.
727+
* Should only be called on the robot editor.
728+
*/
729+
public incorporateNewMechanism(mechanism: storageModule.Mechanism): void {
730+
if (this.module.moduleType !== storageModule.ModuleType.ROBOT) {
731+
return;
732+
}
733+
if (!this.blocklyWorkspace.rendered) {
734+
return;
735+
}
736+
737+
// Register the mechanism in mechanismClassNameToModuleContent so checkMechanism can find it
738+
// during the reload (via getAllComponentsFromMechanism). Note: we must NOT push to
739+
// this.mechanisms here because it references the same array as project.mechanisms, which
740+
// already contains the new mechanism from addModuleToProject — pushing again would duplicate it.
741+
const emptyContent = storageModuleContent.parseModuleContentText(
742+
storageModuleContent.newMechanismContent(this.projectName, mechanism.className));
743+
this.mechanismClassNameToModuleContent[mechanism.className] = emptyContent;
744+
745+
// Capture the live workspace state, append the mechanism block, then reload.
746+
const currentBlocks = Blockly.serialization.workspaces.save(this.blocklyWorkspace);
747+
const tempContentText = storageModuleContent.makeModuleContentText(
748+
this.module, currentBlocks, [], [], [], [], []);
749+
const tempContent = storageModuleContent.parseModuleContentText(tempContentText);
750+
751+
mrcAddMechanismBlockToRobotContent(tempContent, mechanism);
752+
753+
// Remove the existing change listener before reloading to avoid double-registration.
754+
if (this.bindedOnChange) {
755+
this.blocklyWorkspace.removeChangeListener(this.bindedOnChange);
756+
this.bindedOnChange = null;
757+
}
758+
this.modulePathToModuleContent[this.modulePath] = tempContent;
759+
this.loadModuleBlocks();
760+
}
761+
719762
private setPasteLocation(): void {
720763
const copyData = Blockly.clipboard.getLastCopiedData();
721764
if (copyData && copyData.paster === 'block') {

frontend/reactComponents/AddTabDialog.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import * as React from 'react';
2626
import * as commonStorage from '../storage/common_storage';
2727
import * as storageModule from '../storage/module';
2828
import * as storageProject from '../storage/project';
29+
import { Editor } from '../editor/editor';
2930
import ClassNameComponent from './ClassNameComponent';
3031

3132
/** Represents a module item in the dialog. */
@@ -112,7 +113,13 @@ export default function AddTabDialog(props: AddTabDialogProps) {
112113
: storageModule.ModuleType.OPMODE;
113114

114115
await storageProject.addModuleToProject(
115-
props.storage, props.project, moduleType, newClassName);
116+
props.storage, props.project, moduleType, newClassName,
117+
moduleType === storageModule.ModuleType.MECHANISM
118+
? (mech) => {
119+
Editor.getEditorForModulePath(props.project!.robot.modulePath)
120+
?.incorporateNewMechanism(mech);
121+
}
122+
: undefined);
116123
await props.onProjectChanged();
117124

118125
const newModule = storageProject.findModuleByClassName(props.project, newClassName);

0 commit comments

Comments
 (0)