GSoC 2023 with Sugar Labs | Week 7-11 | Music Blocks (v4) project updates

Hi everyone, welcome to a new GSoC’23 Blog, with Sugar Labs – the third blog of the series where I am documenting my complete journey as a contributor for Google Summer of Code 2023 under Sugar Labs for the Music Blocks (v4) project. If you haven’t checked out the previous blog yet, then you should definitely go through it here.

In this blog, I would like to share what I have done during the last month and the overall project updates.

Creating a dummy workspace inside the playground app

My last blog mentioned the different brick types and implementing extent and coords for the args and notches. (#354)
Here are some of the screenshots for them:

So, now using those different types of bricks ("data" | "expression" | "statement" | "block"), I created a dummy workspace inside our playground app (modules/code-builder/playground).

To do this, I first created a dummy WORKSPACES_DATA and assigned an initial tree state to maintain the state. It’ll contain data about each brick and some other metadata. Also, each brick can contain children which will be an Array of different bricks. Here’s a code snippet for the types.

type InstanceMap = {
    data: ModelBrickData;
    expression: ModelBrickExpression;
    statement: ModelBrickStatement;
    block: ModelBrickBlock;
};

export type Brick = {
    id: string;
    type: TBrickType;
    instance: InstanceMap[TBrickType];
    surroundingBricks: { above: string; below: string };
    childBricks: string[];
    coords: TBrickCoords;
    children?: Brick[];
};

export const WORKSPACES_DATA: { id: string; data: Brick[] }[] = [
..........
]

I also created those different brick objects from the different brick classes/models and stored them inside the state. Then, I created a recursive functional React component RenderBricks that will recursively iterate through the overall tree state and render different types of bricks. I also created the BrickFactory wrapper component that’ll be responsible to return and render the exact type of brick based on the brickData prop values. I also refactored and modified the brick components to accept the coords to properly position them inside the SVG (positioning the g tags inside the SVG was another painful task, finally I used the CSS transform property with the translate values from the coords).

function RenderBricks({ brickData }: { brickData: Brick }) {
  return (
    <>
      <BrickFactory brickData={brickData} />
      {brickData.children &&
        brickData.children?.length > 0 &&
        brickData.children.map((child) => <RenderBricks key={child.id} brickData={child} />)}
    </>
  );
}

So after doing all of these, I successfully rendered the initial dummy workspace with different types of bricks nested to each other with different combinations and alignments. Here’s the Pull Request: #361

Here’s a picture of the workspace with a combination of different bricks nested and attached to each other rendering the tree state for the swimlane/workspace.

The dummy workspace state can be found here: modules/code-builder/playground/pages/WorkSpace/data.ts

Moving the Bricks and updating the position and state properly following the rules of Music Blocks

Now to do this, I needed to maintain some sort of centralized state where I could store all of the required dynamic data for each of the bricks along with their IDs (like coords). Also, I needed to update the data for the bricks while dragging them and needed to listen/subscribe to the changes for the data and update the UI according to that.

To do this, I used a simple and efficient library called zustand to create the store, update it, subscribe to the changes and update the React UI according to it. It’s so much simpler, more efficient and easy to use with less boilerplate code than other popular libraries like Redux (Oh, btw I hate Redux and love these proxy-based simple libraries for managing the global client-side state in React applications).

As per the plan, I created the BricksCoordsStore and stored the coordinates of each brick respective to their IDs, and also attached two methods named setCoords (to update the coords value for each brick) and getCoords (to get the coords value of each brick).

I refactored and modified the BrickFactory component to use and encapsulate the move logic and update the positions and central state while keeping those brick components separate and out of any business logic. To achieve the best experience for the move logic while keeping it simple, accessible (should work with keyboard, mouse and touch events) and easy to use, I used the useMove hook from the “React Aria” open-source library.

Also, I created one bounding box indicating the workspace/swimlane and enclosed the move logic so that bricks wouldn’t go beyond that boundary. As I’m updating the global zustand store, and also listening to the changes, the UI is also getting updated/rerendered correctly.

Now comes the harder part, “calculating the move positions for other bricks following the rules of Music Blocks”. When we move one particular brick, we also need to calculate and find the other bricks whose positions need to be updated. For example, if I drag one brick, we also need to update the positions by the same distances of the bricks that are stuck below that specific brick and all of the children and their nested children and the below bricks – definitely some sort of recursive strategy.

That’s why, I created the getBelowBricksIds utility function that uses the recursiveSearch function recursively to find all the IDs of the bricks whose positions needed to be updated when we drag any specific brick, and it returns the Array of IDs of those bricks.

export function getBelowBricksIds(arr: Brick[], item: string): string[] {
    let result: string[] = [];

    function recursiveSearch(arr: Brick[], item: string) {
        arr.forEach((element, index) => {
            if (element.id === item) {
                arr.slice(index + 1, arr.length).map((el) => {
                    result = result.concat(el.childBricks);
                    result = result.concat(el.id);
                });
                return;
            }
            if (Array.isArray(element.children)) {
                recursiveSearch(element.children, item);
            }
        });
    }

    recursiveSearch(arr, item);
    return result;
}

Overall, when we move any specific brick, we need to update the position of that specific brick and move the same amount of position of the bricks which I’m getting from the getBelowBricksIds function. The code for this functionality can be found here.

Here comes everything together and is working perfectly

The last remaining part is definitely detecting the collision when we’re moving those bricks and attaching or detaching them based on the positions and updating the tree state accordingly. I’m working on it and am quite sure it’ll be completed within a few days. Stay tuned to read a new blog on that.

So far, I hope you’ve enjoyed reading about my progress. I’ll be back with new updates about my GSoC journey soon.

Also, don’t forget to drop a star on the project GitHub repo! ⭐
Link: https://github.com/sugarlabs/musicblocks-v4

You can connect with me on LinkedIn. Additionally, you can follow me on GitHub and Twitter, it will be appreciated.

Thank you for reading!