diff --git a/.gitignore b/.gitignore
index 3da87c9a..191b596f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
+
+*storybook.log
\ No newline at end of file
diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc
index 25310649..c2481488 100644
--- a/.markdownlint.jsonc
+++ b/.markdownlint.jsonc
@@ -81,8 +81,6 @@
"tables": true,
// Include headings
"headings": true,
- // Include headings
- "headers": true,
// Strict length checking
"strict": false,
// Stern length checking
@@ -118,9 +116,7 @@
// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
"MD024": {
// Only check sibling headings
- "allow_different_nesting": true,
- // Only check sibling headings
- "siblings_only": false
+ "siblings_only": true
},
// MD025/single-title/single-h1 - Multiple top-level headings in the same document
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2ba7ff8c..fd7a62d3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,7 @@
{
- "editor.rulers": [100],
+ "editor.rulers": [
+ 100
+ ],
"editor.renderWhitespace": "boundary",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"javascript.updateImportsOnFileMove.enabled": "always",
@@ -25,7 +27,7 @@
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
- "editor.wordBasedSuggestions": false
+ "editor.wordBasedSuggestions": "off"
},
"[markdown]": {
"editor.wordWrap": "on",
diff --git a/modules/masonry/.storybook/main.ts b/modules/masonry/.storybook/main.ts
new file mode 100644
index 00000000..cd4e8a1d
--- /dev/null
+++ b/modules/masonry/.storybook/main.ts
@@ -0,0 +1,37 @@
+import type { UserConfigExport } from 'vite';
+
+import path from 'path';
+import { mergeConfig } from 'vite';
+
+// -------------------------------------------------------------------------------------------------
+
+function resolve(rootPath: string) {
+ return path.resolve(__dirname, '..', rootPath);
+}
+
+export default {
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(tsx|ts|jsx|js)'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ ],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+ docs: {
+ autodocs: 'tag',
+ },
+ async viteFinal(config: UserConfigExport) {
+ return mergeConfig(config, {
+ resolve: {
+ alias: {
+ '@': resolve('src'),
+ '@res': resolve('../../res'),
+ },
+ extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'],
+ },
+ });
+ },
+};
diff --git a/modules/masonry/.storybook/preview.ts b/modules/masonry/.storybook/preview.ts
new file mode 100644
index 00000000..ae3ab20a
--- /dev/null
+++ b/modules/masonry/.storybook/preview.ts
@@ -0,0 +1,13 @@
+import '@res/scss/base.scss';
+
+export const parameters = {
+ actions: {
+ argTypesRegex: '^on[A-Z].*',
+ },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+};
diff --git a/modules/masonry/docs/architecture/MasonryDFD.drawio b/modules/masonry/docs/architecture/MasonryDFD.drawio
new file mode 100644
index 00000000..80205179
--- /dev/null
+++ b/modules/masonry/docs/architecture/MasonryDFD.drawio
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/masonry/docs/architecture/MasonryDFD.drawio.png b/modules/masonry/docs/architecture/MasonryDFD.drawio.png
new file mode 100644
index 00000000..114eb4aa
Binary files /dev/null and b/modules/masonry/docs/architecture/MasonryDFD.drawio.png differ
diff --git a/modules/masonry/docs/architecture/Processes.md b/modules/masonry/docs/architecture/Processes.md
new file mode 100644
index 00000000..ffff0d69
--- /dev/null
+++ b/modules/masonry/docs/architecture/Processes.md
@@ -0,0 +1,204 @@
+# Processes for Data Flow Diagram
+
+## Level 0: Masonry Framework Communication with MusicBlocks
+
+1. **Load Configuration**:
+ - Input: Configuration File
+ - Output: Initialized System
+
+2. **Save Configuration**:
+ - Input: Current State
+ - Output: Updated Configuration File
+
+3. **Generate Syntax Tree**:
+ - Input: Brick Stack Data
+ - Output: Syntax Tree
+
+4. **Parse Syntax Tree**:
+ - Input: Syntax Tree
+ - Output: Executable Actions
+
+## Level 1: Interaction between Brick, Palette, Workspace, and Stack of Bricks
+
+### Bricks
+
+1. **Initialize Brick**:
+ - Input: Brick Properties
+ - Output: Initialized Brick
+
+2. **Provide Brick Properties**:
+ - Input: Brick ID from Workspace
+ - Output: Brick Properties to Workspace
+
+### Palette
+
+1. **Load Brick List**:
+ - Input: Configuration Settings
+ - Output: List of Bricks
+
+2. **Select Brick**:
+ - Input: Brick Selection
+ - Output: Selected Brick Properties to Workspace
+ (How this works is, The palette will have a loaded list of SVGs. When you drag one from palette on
+ to the workspace, the brick will be created on the workspace whos id matches to the one in brick)
+
+### Workspace
+
+1. **Add Brick to Workspace**:
+ - Input: Brick Properties from Palette
+ - Output: Updated Workspace
+
+2. **Update Brick Position**:
+ - Input: Brick ID and Position Data
+ - Output: Updated Brick Position in Workspace
+
+3. **Connect Bricks**:
+ - Input: Brick IDs and Connection Data
+ - Output: Updated Brick Stack in Workspace
+
+4. **Disconnect Bricks**:
+ - Input: Brick IDs
+ - Output: Updated Brick Stack in Workspace
+
+5. **Save Workspace State**:
+ - Input: Current Workspace Data
+ - Output: Saved Workspace State
+
+### Stack of Bricks
+
+1. **Initialize Stack**:
+ - Input: Brick Stack Data
+ - Output: Initialized Stack
+
+2. **Provide Stack Properties**:
+ - Input: Stack ID from Workspace
+ - Output: Stack Properties to Workspace
+
+## Level 2: Detailed Interaction within MVC Architecture
+
+### Model
+
+1. BrickModel:
+ - Properties:
+ - brickType
+ - originalColor
+ - hoverColor
+ - disconnectedColor
+ - executionColor
+ - highlightState
+ - shape
+ - sprites
+ - labels
+ - inputPorts
+ - outputPorts
+ - editableTextLabels
+ - Methods:
+ - setHighlightState(state)
+ - updateProperties(properties)
+ - updateLabels(labels)
+ - updatePorts(inputPorts, outputPorts)
+2. StackModel:
+ - Properties:
+ - connectedBricks
+ - startPosition
+ - validationRules
+ - collapsibleState
+ - Methods:
+ - addBrick(brick)
+ - removeBrick(brick)
+ - updateProperties(properties)
+ - validateStack()
+ - setCollapsibleState(state)
+3. PaletteModel:
+ - Properties:
+ - availableBricks
+ - categories
+ - searchQuery
+ - filters
+ - Methods:
+ - loadBricks(bricks)
+ - updateBrickAvailability(brick, available)
+ - categorizeItems(categories)
+ - filterItems(filters)
+ - searchItems(query)
+4. WorkspaceModel:
+ - Properties:
+ - connectedStacks
+ - brickPositions
+ - zoomLevel
+ - undoRedoStack
+ - Methods:
+ - addStack(stack)
+ - removeStack(stack)
+ - updateBrickPosition(brick, position)
+ - connectBricks(brick1, brick2)
+ - disconnectBricks(brick1, brick2)
+ - deleteBrick(brick)
+ - zoomIn()
+ - zoomOut()
+ - undo()
+ - redo()
+
+### View
+
+1. BrickView:
+ - Methods:
+ - renderBrick(brick)
+ - updateBrickAppearance(brick)
+ - renderInlineTextEditing(brick)
+ - renderContextMenu(brick)
+2. StackView:
+ - Methods:
+ - renderStack(stack)
+ - updateStackAppearance(stack)
+ - renderValidationFeedback(stack)
+ - renderCollapsibleState(stack)
+3. PaletteView:
+ - Methods:
+ - renderPalette(palette)
+ - updatePaletteAppearance(palette)
+ - renderCategories(categories)
+ - renderSearchBar(searchQuery)
+ - renderFilters(filters)
+4. WorkspaceView:
+ - Methods:
+ - renderWorkspace(workspace)
+ - updateWorkspaceAppearance(workspace)
+ - handleUserInteractions(interaction)
+ - renderZoomControls(zoomLevel)
+ - renderUndoRedoButtons(undoRedoStack)
+
+### Controller
+
+1. BrickController:
+ - Methods:
+ - handleBrickPropertyChange(brick, properties)
+ - handleBrickHighlightStateChange(brick, state)
+ - handleInlineTextEditing(brick, text)
+ - handleContextMenuAction(brick, action)
+2. StackController:
+ - Methods:
+ - handleAddBrick(stack, brick)
+ - handleRemoveBrick(stack, brick)
+ - handleStackPropertyChange(stack, properties)
+ - handleStackValidation(stack)
+ - handleCollapsibleStateChange(stack, state)
+3. PaletteController:
+ - Methods:
+ - handleBrickLoad(palette, bricks)
+ - handleBrickAvailabilityChange(palette, brick, available)
+ - handleCategorization(palette, categories)
+ - handleFiltering(palette, filters)
+ - handleSearching(palette, query)
+4. WorkspaceController:
+ - Methods:
+ - handleAddStack(workspace, stack)
+ - handleRemoveStack(workspace, stack)
+ - handleBrickPositionChange(workspace, brick, position)
+ - handleBrickConnection(workspace, brick1, brick2)
+ - handleBrickDisconnection(workspace, brick1, brick2)
+ - handleBrickDeletion(workspace, brick)
+ - handleZoomIn(workspace)
+ - handleZoomOut(workspace)
+ - handleUndo(workspace)
+ - handleRedo(workspace)
diff --git a/modules/masonry/docs/functional-specification/Masonry_Design_Document.md b/modules/masonry/docs/functional-specification/Masonry_Design_Document.md
new file mode 100644
index 00000000..12f26331
--- /dev/null
+++ b/modules/masonry/docs/functional-specification/Masonry_Design_Document.md
@@ -0,0 +1,388 @@
+# Music Blocks v4 : Masonry Design Document
+
+## Overview
+
+1. **Short Description of the Product**
+ - The Masonry (previously called Project Builder) in Music Blocks v4 facilitates graphical brick
+ -based music composition, offering various Brick types such as Start, Rhythm, Note, Pitch, and
+ Instrument Bricks. Each brick represents a specific functionality, enabling users, especially
+ children, to visually create music programs. The Masonry module simplifies the process of
+ selecting and arranging bricks to generate music sequences.
+
+2. **Key Features**
+ - Enhance the brick library with comprehensive functionalities and render the stack of bricks.
+ - Implement collision detection and enhance the user interface for a seamless user experience.
+ - Add text to bricks (SVGs) for customization and personalization.
+ - Introduce a palette feature, allowing for effortless music composition through intuitive
+ drag-and-drop functionality.
+ - Integrate Music Blocks v4 with Masonry to streamline music program creation.
+ - Address bugs and make overall improvements to enhance the tool's performance and usability.
+
+3. **Main User Activities**
+ - Interacting with the bricks using the palette.
+ - Composing music using visual programming bricks/stack of bricks.
+ - Editing music sequences dynamically.
+
+4. **Subsystems**
+ - Palette Subsystem: Manages the palette interface within Music Blocks, providing a selection of
+ bricks for users to drag and drop into the workspace.
+ - Workspace Subsystem: Controls the workspace area where users can arrange bricks and create
+ music compositions.
+ - Brick Stack Subsystem: Handles the creation and management of stacks of bricks within the
+ workspace, allowing users to combine bricks to form musical sequences.
+ - Collision Detection Subsystem: Implements collision detection functionality within the
+ workspace, ensuring that bricks interact appropriately to prevent overlapping or conflicting arrangements.
+
+5. **Additional Functionality and Design**
+ - Implementation of a MusicBlocks guide button at the top of the interface for user convenience.
+ - Integration of a collision detection UI inspired by the Brickly game by Google, enhancing user
+ experience and interaction feedback.
+ - Optimization of the palette by combining similar types of bricks, reducing clutter and
+ improving usability.
+ - Enhancement of the search functionality to facilitate easier navigation and selection of bricks
+ within the palette.
+
+6. **Purpose**
+ - The purpose of this document is to outline the design and architecture of Masonry framework for
+ Music Blocks v4
+
+7. **Scope**
+ - This document covers the technical details and design considerations of Music Blocks v4,
+ including its features and subsystems.
+
+8. **Audience**
+ - The intended audience includes developers, contributors, and members involved in the development
+ and maintenance of Music Blocks v4.
+
+9. **Definitions and Abbreviations**
+ - **Masonry:** The term used to describe the replication and enhancement of the functionality
+ related to bricks from the palette, stack of bricks, and other related components of the
+ project, aimed at improving their functionality and effectiveness.
+
+ [Screencast from 13-05-24 12:15:30 PM IST.webm](https://github.com/Karan-Palan/musicblocks-v4/assets/143683619/ae9df412-8b3a-4930-8635-ad89da828ba9)
+
+10. **References**
+
+- [Link to Music Blocks v3 project](https://github.com/sugarlabs/musicblocks)
+- [Link to Masonry Framework](https://github.com/sugarlabs/musicblocks-v4/tree/develop/modules/code-builder)
+
+## Requirements, Wiki Storages and Docs
+
+1. **Requirements**
+ - Functional requirements: Dynamic editing, text addition, UI enhancements, palette feature,
+ integration with Music Blocks v4 project.
+ - Non-functional requirements: Performance, scalability, maintainability.
+
+2. **Wiki Storages**
+ - [Link to project documentation](https://github.com/sugarlabs/musicblocks/blob/master/guide/README.md)
+
+3. **Docs and Responsible Entities**
+ - Documentation maintained by project contributors.
+ - Responsible entities: Project maintainers, contributors.
+
+4. **Roles, Responsibilities, and Assumptions**
+ - Roles: Developers, contributors, project maintainers.
+ - Responsibilities: Implementing features, reviewing code, documenting changes.
+ - Assumptions: Basic understanding of React, JavaScript/TypeScript, and information about Music
+ Blocks software.
+
+## Architecture and Requirements Diagram
+
+To be added
+
+### Design Specification
+
+#### 1. Workspace
+
+- Canvas Area:
+ - Large central area for creating and manipulating musical compositions
+ - Resizable canvas to accommodate compositions of varying sizes
+ - Optional background grid or ruled lines for precise Brick placement
+
+- Grid System:
+ - Configurable grid spacing and subdivisions
+ - Snap-to-grid functionality for aligning Bricks to grid lines
+ - Visual indicators (e.g., dotted lines, highlights) for grid lines and snap points
+
+- Staff Rendering:
+ - Multiple staff lines for representing different instruments or voices
+ - Customizable staff parameters (number of lines, clef, key signature, time signature)
+ - Dynamic staff layout and spacing based on the composition's content
+
+- Brick Connections:
+ - Visual representation of connections between Bricks (e.g., lines, curves, bezier paths)
+ - Color-coding or highlighting for different connection types or data flows
+ - Animated transitions or visual cues when establishing or breaking connections
+
+- Visual Feedback:
+ - Error indicators (e.g., red outlines, warning icons) for invalid Brick placements or connections
+ - Compatibility indicators (e.g., green highlights) for valid Brick combinations
+ - Tooltips or popups for providing additional information or guidance
+
+- Navigation and Viewing:
+ - Panning and scrolling functionality for navigating large compositions
+ - Zoom controls for adjusting the workspace scale and level of detail
+ - Minimap or overview panel for a bird's-eye view of the entire composition
+
+- Workspace Customization:
+ - Options to hide or show grid lines, staff lines, or other visual aids
+ - Configurable workspace background color or theme
+ - Ability to save and load workspace layouts or configurati
+
+#### 2. Palette
+
+- Layout and Organization:
+ - Collapsible/expandable categories or sections for different Brick types
+ - Customizable order and arrangement of categories
+ - Visual separators or dividers between categories
+
+- Brick Previews:
+ - Thumbnail or icon previews for each Brick within a category
+ - Tooltips or pop-ups displaying Brick names and brief descriptions
+ - Color-coding or visual cues for distinguishing different Brick types
+
+- Search and Filtering:
+ - Search bar or input field for locating specific Bricks by name or keyword
+ - Filter options for narrowing down Bricks based on category, type, or properties
+ - Live search results or suggestions as the user types
+
+- Drag and Drop:
+ - Ability to drag and drop Bricks from the palette onto the workspace
+ - Visual indicators (e.g., ghost preview, outline) for valid drop locations
+ - Snap-to-grid or precise positioning when dropping Bricks
+
+- Brick Creation:
+ - Options for creating new Bricks or Brick instances directly from the palette
+ - Context menus or shortcuts for duplicating or cloning existing Bricks
+ - Visual feedback or animations when creating or instantiating new Bricks
+
+- Customization:
+ - User-defined categories or custom groupings for organizing Bricks
+ - Ability to rename, reorder, or hide/show specific categories
+ - Import/export functionality for sharing or backing up custom palette configurations
+
+- Palette Behavior:
+ - Dockable or floating palette panel for flexible positioning
+ - Resizable palette window or panel for adjusting its size
+ - Auto-hide or collapse functionality for maximizing workspace area
+
+#### 3. Bricks
+
+- Shape and Appearance:
+ - Distinct geometric shapes (rectangles, circles, hexagons) for different Brick categories
+ - Color schemes and palettes for visually differentiating Brick types
+ - Textured or patterned backgrounds for certain Brick categories (e.g., control structures)
+
+- Brick Labels and Icons:
+ - Clear and concise text labels describing the Brick's function
+ - Intuitive icons or symbols representing the Brick's purpose
+ - Customizable font styles, sizes, and colors for labels and icons
+
+- Input/Output Ports:
+ - Shaped ports or connectors for linking Bricks together
+ - Color-coding or visual cues for indicating compatible port types
+ - Tooltips or labels explaining the purpose and data type of each port
+
+- Brick Parameters:
+ - Editable fields within Bricks for adjusting parameters (e.g., note values, durations)
+ - Drop-down menus or selectors for choosing from predefined parameter options
+ - Visual indicators like sliders, knobs, or dials for continuous parameter adjustments
+
+- Resizing and Scaling:
+ - Resize handles (corners, edges) for adjusting Brick dimensions
+ - Proportional scaling or aspect ratio locking options
+ - Dynamic scaling of Brick contents (text, icons) during resizing
+
+- Rotation and Flipping:
+ - Rotation handles or controls for changing Brick orientation
+ - Flip or mirror functionality for reversing Brick placement
+ - Snap-to-angle or constrained rotation options (e.g., 90-degree increments)
+
+- Cloning and Duplication:
+ - Duplicate or clone functionality for creating copies of existing Bricks
+ - Visual previews or ghosted outlines for cloned Brick instances
+ - Options for shallow cloning (duplicating the Brick) or deep cloning (including nested Bricks)
+
+- Advanced Editing:
+ - Context menus or dedicated editors for advanced Brick customization
+ - Visual indicators or badges for displaying Brick metadata (e.g., unique IDs)
+ - Color-coding or visual cues for distinguishing different Brick states or modes
+
+#### 4. Brick Connections
+
+- Connection Styles:
+ - Various visual styles for rendering connections (straight lines, curves, bezier paths)
+ - Customizable line thickness, colors, and patterns for different connection types
+ - Animated transitions or visual effects when establishing or breaking connections
+
+- Connection Routing:
+ - Automatic routing algorithms for avoiding overlaps and minimizing crossed connections
+ - Manual routing options for overriding automatic layouts
+ - Visual guides or markers for assisting with precise connection routing
+
+- Connection Labels:
+ - Ability to add labels or annotations along connections
+ - Customizable label styles (font, color, background) for different connection types
+ - Positioning options for labels (centered, aligned to start/end)
+
+- Data Flow Visualization:
+ - Visual indicators or animations for showing data flow direction along connections
+ - Color-coding or highlighting for different data types or flows
+ - Tooltips or pop-ups for displaying data values or previews
+
+- Connection Validation:
+ - Visual feedback for valid and invalid connections (e.g., green/red highlights)
+ - Error messages or tooltips explaining incompatible connections
+ - Ability to temporarily disable validation for advanced use cases
+
+- Connection Editing:
+ - Options for rerouting, splitting, or merging connections
+ - Context menus or shortcuts for quickly editing connection properties
+ - Undo/redo functionality for connection editing operations
+
+- Connection Grouping:
+ - Ability to group multiple connections together for better organization
+ - Visual boundaries or outlines for defining connection groups
+ - Collapsible or expandable groups for managing complexity
+
+#### 5. Collision Detection and Snapping
+
+- Brick Bounding Boxes:
+ - Visual representations of Brick bounding boxes or hit areas
+ - Configurable padding or margins around Brick boundaries
+ - Color-coding or highlighting of bounding boxes for debugging or visualization
+
+- Proximity Detection:
+ - Visual indicators or highlights when Bricks are within a specified proximity
+ - Adjustable proximity thresholds or ranges for different snapping behaviors
+ - Tooltips or pop-ups displaying the current proximity distance
+
+- Snap-to-Grid:
+ - Visual guides or markers for grid lines and snap points
+ - Configurable grid spacing and subdivision settings
+ - Adjustable snapping strength or magnetic attraction to the grid
+
+- Snap-to-Brick:
+ - Alignment guides or visual cues for snapping Bricks to other Brick edges or centers
+ - Customizable snapping priorities (e.g., snap to centers first, then edges)
+ - Temporary visual previews of snapped positions before releasing the Brick
+
+- Snap-to-Connection:
+ - Visual indicators or highlights for compatible connection points between Bricks
+ - Automatic connection establishment when Bricks are snapped together
+ - Animations or visual effects during the snapping and connection process
+
+- Overlap Prevention:
+ - Visual feedback or error indicators for overlapping or invalid Brick placements
+ - Automatic repositioning or nudging of Bricks to avoid overlaps
+ - User-defined rules or constraints for allowing or preventing Brick overlaps
+
+- Snapping Customization:
+ - Options to enable or disable specific snapping behaviors (grid, Brick, connection)
+ - User-defined snapping preferences or profiles
+ - Import/export functionality for sharing custom snapping configurations
+
+#### 6. Brick Editing and Customization
+
+- Inline Editing:
+ - Editable text fields within Bricks for modifying labels, values, or parameters
+ - Visual indicators or highlights for active inline editing mode
+ - Validation and error feedback for invalid inputs or out-of-range values
+
+- Drop-down Menus:
+ - Drop-down lists or selectors within Bricks for choosing from predefined options
+ - Customizable visual styles (fonts, colors, icons) for drop-down menu items
+ - Tooltips or previews for displaying additional information about each option
+
+- Context Menus:
+ - Right-click or long-press context menus for accessing Brick-specific actions
+ - Hierarchical or nested menus for organizing related actions and options
+ - Keyboard shortcuts or mnemonics for quick access to frequently used actions
+
+- Dedicated Editors:
+ - Modal dialogs or dedicated panels for advanced Brick customization
+ - Visual editing interfaces (e.g., piano roll, rhythm editors) for specialized parameters
+ - Undo/redo functionality within dedicated editors for tracking changes
+
+- Undo/Redo Indicators:
+ - Visual indicators or badges for displaying the current undo/redo state
+ - Animations or visual effects when undoing or redoing Brick editing actions
+ - Tooltips or pop-ups showing a preview of the undo/redo operation
+
+#### 7. Musical Notation Rendering
+
+- Notation Styles:
+ - Traditional staff notation with notes, rests, and other musical symbols
+ - Alternative representations like piano roll, guitar tablature, or custom notations
+ - Customizable notation styles (fonts, colors, line spacing) for different instruments
+
+- Real-time Updates:
+ - Synchronized updates to the notation as Bricks are added, removed, or modified
+ - Smooth transitions or animations when updating the notation
+ - Visual indicators or highlights for recently changed or updated notation elements
+
+- Notation Switching:
+ - Options or controls for switching between different notation styles or views
+ - Visual previews or thumbnails of each notation style for easy identification
+ - Customizable keyboard shortcuts or hotkeys for quickly switching notations
+
+- Notation Overlays:
+ - Ability to overlay multiple notation styles or representations simultaneously
+ - Visual separators or dividers between different notation layers
+ - Customizable transparency or opacity settings for each notation layer
+
+- Playback Integration:
+ - Synchronized highlighting or animations within the notation during audio playback
+ - Visual indicators or markers for the current playback position
+ - Customizable playback cursors or beat markers for different notation styles
+
+#### 8. Audio Playback and Visualization
+
+- Playback Controls:
+ - Intuitive play, pause, stop, and seek buttons or controls
+ - Visual feedback for playback state (playing, paused, stopped)
+ - Seek bar or timeline for navigating through the composition
+
+- Audio Visualizations:
+ - Synchronized visualizations like piano roll, waveform, or custom graphical representations
+ - Configurable visualization styles (colors, themes, rendering modes)
+ - Visual indicators or animations synchronized with the audio playback
+
+- Instrument Selection:
+ - Visual representations (icons, thumbnails) for different instrument sounds
+ - Categorization or grouping of instruments (e.g., by family, genre)
+ - Tooltips or previews for auditioning instrument sounds
+
+- Playback Overlays:
+ - Ability to overlay multiple visualizations or instrument views simultaneously
+ - Visual separators or dividers between different overlay layers
+ - Customizable transparency or opacity settings for each overlay layer
+
+#### 9. User Interface and Interactions
+
+- Responsive Layout:
+ - Adaptive and responsive design for different screen sizes and resolutions
+ - Automatic layout adjustments and reflow for optimal viewing experience
+ - Optional full-screen or immersive mode for maximizing workspace area
+
+- Keyboard Shortcuts:
+ - Visual indicators or tooltips for available keyboard shortcuts and hotkeys
+ - Customizable keyboard shortcut mappings and assignments
+ - Conflict resolution or priority handling for overlapping shortcut combinations
+
+- Toolbars and Menus:
+ - Configurable toolbars and menus for quick access to frequently used features
+ - Customizable toolbar and menu layouts (docking, floating, auto-hide)
+ - Visual indicators or badges for displaying tool states or modes
+
+- Themes and Customization:
+ - Predefined color themes and visual styles for different preferences
+ - Customizable color schemes, font styles, and icon sets
+ - Import/export functionality for sharing custom theme configurations
+
+- Accessibility:
+ - High-contrast mode or themes for improved visibility
+ - Screen reader support and appropriate labeling for accessibility
+ - Adjustable font sizes and zoom levels for better readability
+
+---
diff --git a/modules/masonry/docs/functional-specification/PRD.md b/modules/masonry/docs/functional-specification/PRD.md
new file mode 100644
index 00000000..b3b1154d
--- /dev/null
+++ b/modules/masonry/docs/functional-specification/PRD.md
@@ -0,0 +1,162 @@
+
+# Product Requirements Document (PRD): Masonry Framework
+
+## 1. Bricks
+
+### a. Brick Types
+
+#### 1.Data Bricks: These serve as inputs for other bricks and come in two types
+
+#### hardcoded and editable
+
+- Hardcoded Data Brick: Fixed values that cannot be changed by the user. Examples include predefined
+ note values, counts, etc.
+
+ 
+- Editable Data Brick: Values that can be modified by the user. When clicked, these bricks open a
+text editor or a dropdown menu for user input, allowing customization of note names, pitches, etc.
+
+ 
+
+#### 2. Expression Bricks: Takes values as input, returns a value as output
+
+ 
+
+#### 3. Statement Bricks: These define actions to be taken
+
+ 
+
+#### 4. Block Bricks: Contain nesting, also execute something like the statement
+
+#### bricks. Takes 0 or more arguments
+
+ 
+
+### b. Brick Appearance
+
+- **Distinct Shapes**: Each brick type has a unique shape to differentiate its function visually.
+- **Colors**:
+ 1. **Original Color**: Each brick has a unique color that represents its type.
+ 2. **Hover Color**: When a user hovers over a brick, it changes to a distinct color to indicate
+ it is selectable.
+ 3. **Disconnected Color**: If a brick is not connected to the stack, it turns gray to indicate
+ it is inactive.
+ 4. **Execution Color**: When a brick is executed, it changes to a darker shade of its original
+ color to show that it has been activated.
+- **Sprites**: Visual symbols that indicate specific functions or properties of the brick. Some
+bricks may have sprites (like the start brick), while others may not.
+- **Labels**:
+ 1. **Functionality Labels**: Text labels that indicate the function of the brick.
+ 2. **Argument Labels**: Text labels that indicate the arguments or parameters that need to be
+ provided for the brick's function.
+- **Input/Output Ports**: Connectors that visually represent where bricks can attach to each other.
+- **Editable Text Labels/Fields**: Users can input data directly into the bricks, such as note names,
+ durations, and numerical values.
+
+ **Side Note:** If we want to implement a design similar to Scratch in the future, we can consider
+the following approach for connecting blocks:
+
+- In Scratch, blocks are connected horizontally in a row for sequential execution. Each block has a
+tab at the bottom and a notch at the top, allowing them to snap together in a linear sequence. This
+design makes it clear which blocks will execute in order.
+
+### c. Brick Interactions
+
+- **Inline Text Editing**: Users can click on text fields within bricks to edit labels and values directly.
+ - Some bricks only open an inline text editor when clicked.
+
+ 
+
+ - Other bricks open both an inline text editor and a context menu for additional options.
+
+ 
+- **Context Menus**: For more complex properties, a separate interface allows detailed configuration.
+
+ 
+- **Connection Types**:
+ 1. **Argument Connections**: Bricks can be connected to input arguments of other bricks. This
+ allows for passing data or parameters into the brick’s function.
+ 2. **Brick-to-Brick or Stack Connections**: Bricks can be connected directly to other bricks or
+ to a stack of bricks. This enables building complex sequences and structures by chainingbricks
+ together.
+
+ 
+
+## 2. Stack of Bricks
+
+### a. Stack Validation
+
+- **Visual Feedback**: Indicators show whether brick combinations are valid.
+
+- **Error Indicators**: Explanations for incompatible connections help users troubleshoot.
+ - Add a reddish boundary for users to easily tell whether the bricks are mergeable or not.
+
+ 
+
+- **Disable Validation**: Temporarily turn off validation for complex or experimental setups.
+
+ **Note** - this is up for further discussion
+
+### b. Stack Editing
+
+- **Connection Editing**: Options for re-positioning, disconnecting, or connecting.
+- **Quick Edit Shortcuts**: Context menus or keyboard shortcuts speed up editing.
+
+### c. Stack Grouping
+
+- **Collapsible Groups**: Groups can be collapsed or expanded to manage complexity.
+
+ 
+
+## 3. Palette
+
+### a. **Layout and Organization**
+
+- Collapsible/expandable categories or sections for different Brick types
+- Visual separators or dividers between categories
+- Customizable order and arrangement of categories
+
+ 
+
+### b. **Brick Previews**
+
+- Tooltips or pop-ups displaying Brick names and brief descriptions
+
+ 
+
+- Color-coding or visual cues for distinguishing different Brick types
+
+### c. **Search and Filtering**
+
+- Search bar or input field for locating specific Bricks by name or keyword
+- Filter options for narrowing down Bricks based on category, type, or properties
+- Live search results or suggestions as the user types
+ - Searchbar in Musicblocks as of now:
+
+ 
+
+ - Searchbar design to be implemented:
+
+ 
+ The idea here is to have a fixed searchbar on the left side of the workspace through which
+ users can search for bricks, group them etc.
+ Note - It is just a one big list and categories on the left are positions on the list.
+
+### d. **Drag and Drop**
+
+- Ability to drag and drop Bricks from the palette onto the workspace.
+- Visual indicators (e.g., ghost preview, outline) for valid drop locations.
+
+ 
+
+ - While dragging a brick from the palette, the brick should temporarily disappear from the palette
+ until it is placed in the workspace.
+
+## 4. Workspace
+
+
+
+- **Cloning/Duplication**: Users can easily create copies of bricks for repeated use.
+- **Scaling and Rotation**: Bricks can be resized and rotated to fit the workspace better.
+- **Undo/Redo**: Users can revert or reapply changes to their stacks.
+- **Removal/Deletion of Bricks** : Users can remove/delete bricks
diff --git a/modules/masonry/docs/images/image-1.png b/modules/masonry/docs/images/image-1.png
new file mode 100644
index 00000000..e732c6fa
Binary files /dev/null and b/modules/masonry/docs/images/image-1.png differ
diff --git a/modules/masonry/docs/images/image-10.png b/modules/masonry/docs/images/image-10.png
new file mode 100644
index 00000000..adde8621
Binary files /dev/null and b/modules/masonry/docs/images/image-10.png differ
diff --git a/modules/masonry/docs/images/image-11.png b/modules/masonry/docs/images/image-11.png
new file mode 100644
index 00000000..ad96d427
Binary files /dev/null and b/modules/masonry/docs/images/image-11.png differ
diff --git a/modules/masonry/docs/images/image-12.png b/modules/masonry/docs/images/image-12.png
new file mode 100644
index 00000000..ab099f0f
Binary files /dev/null and b/modules/masonry/docs/images/image-12.png differ
diff --git a/modules/masonry/docs/images/image-13.png b/modules/masonry/docs/images/image-13.png
new file mode 100644
index 00000000..e1b48b41
Binary files /dev/null and b/modules/masonry/docs/images/image-13.png differ
diff --git a/modules/masonry/docs/images/image-14.png b/modules/masonry/docs/images/image-14.png
new file mode 100644
index 00000000..e1943ab4
Binary files /dev/null and b/modules/masonry/docs/images/image-14.png differ
diff --git a/modules/masonry/docs/images/image-15.png b/modules/masonry/docs/images/image-15.png
new file mode 100644
index 00000000..b504e70a
Binary files /dev/null and b/modules/masonry/docs/images/image-15.png differ
diff --git a/modules/masonry/docs/images/image-16.png b/modules/masonry/docs/images/image-16.png
new file mode 100644
index 00000000..ab8bdd7e
Binary files /dev/null and b/modules/masonry/docs/images/image-16.png differ
diff --git a/modules/masonry/docs/images/image-17.png b/modules/masonry/docs/images/image-17.png
new file mode 100644
index 00000000..c2cf47dd
Binary files /dev/null and b/modules/masonry/docs/images/image-17.png differ
diff --git a/modules/masonry/docs/images/image-18.png b/modules/masonry/docs/images/image-18.png
new file mode 100644
index 00000000..9d1414f7
Binary files /dev/null and b/modules/masonry/docs/images/image-18.png differ
diff --git a/modules/masonry/docs/images/image-19.png b/modules/masonry/docs/images/image-19.png
new file mode 100644
index 00000000..4b7f10cc
Binary files /dev/null and b/modules/masonry/docs/images/image-19.png differ
diff --git a/modules/masonry/docs/images/image-2.png b/modules/masonry/docs/images/image-2.png
new file mode 100644
index 00000000..f9d37d75
Binary files /dev/null and b/modules/masonry/docs/images/image-2.png differ
diff --git a/modules/masonry/docs/images/image-3.png b/modules/masonry/docs/images/image-3.png
new file mode 100644
index 00000000..3924e09e
Binary files /dev/null and b/modules/masonry/docs/images/image-3.png differ
diff --git a/modules/masonry/docs/images/image-4.png b/modules/masonry/docs/images/image-4.png
new file mode 100644
index 00000000..7af5aa5c
Binary files /dev/null and b/modules/masonry/docs/images/image-4.png differ
diff --git a/modules/masonry/docs/images/image-5.png b/modules/masonry/docs/images/image-5.png
new file mode 100644
index 00000000..bbe377f1
Binary files /dev/null and b/modules/masonry/docs/images/image-5.png differ
diff --git a/modules/masonry/docs/images/image-6.png b/modules/masonry/docs/images/image-6.png
new file mode 100644
index 00000000..9c13d381
Binary files /dev/null and b/modules/masonry/docs/images/image-6.png differ
diff --git a/modules/masonry/docs/images/image-7.png b/modules/masonry/docs/images/image-7.png
new file mode 100644
index 00000000..00d3a06e
Binary files /dev/null and b/modules/masonry/docs/images/image-7.png differ
diff --git a/modules/masonry/docs/images/image-8.png b/modules/masonry/docs/images/image-8.png
new file mode 100644
index 00000000..d46522d6
Binary files /dev/null and b/modules/masonry/docs/images/image-8.png differ
diff --git a/modules/masonry/docs/images/image-9.png b/modules/masonry/docs/images/image-9.png
new file mode 100644
index 00000000..eb75758c
Binary files /dev/null and b/modules/masonry/docs/images/image-9.png differ
diff --git a/modules/masonry/docs/images/image.png b/modules/masonry/docs/images/image.png
new file mode 100644
index 00000000..9242044d
Binary files /dev/null and b/modules/masonry/docs/images/image.png differ
diff --git a/modules/masonry/docs/technical-specification/Brick.md b/modules/masonry/docs/technical-specification/Brick.md
new file mode 100644
index 00000000..5d08a1ca
--- /dev/null
+++ b/modules/masonry/docs/technical-specification/Brick.md
@@ -0,0 +1,127 @@
+# Brick
+
+## 1. **`model.ts`**: Abstract Classes
+
+This file contains the abstract classes that define the blueprint for different types of bricks.
+These classes are not instantiated directly; instead, they are extended by concrete classes to provide
+specific implementations.
+
+### Abstract Classes
+
+- **`BrickModel`**: The base class for all brick types.
+ - **Properties:**
+ - `uuid: string` - Unique identifier.
+ - `name: string` - Name for internal bookkeeping.
+ - `kind: TBrickKind` - Represents the kind (e.g., "instruction" or "argument").
+ - `type: TBrickType` - Represents the type (e.g., "data", "expression", "statement", "block").
+ - `label: string` - Primary label for display.
+ - `glyph: string` - Optional glyph icon.
+ - `colorBg`, `colorFg`, `colorBgHighlight`, `colorFgHighlight`, `outline` - Colors for display.
+ - `highlighted: boolean` - State indicating whether the brick is highlighted.
+ - `scale: number` - Scale factor for rendering.
+ - **Abstract Methods:**
+ - `get boundingBox(): TExtent` - Returns the bounding box dimensions of the brick.
+ - `get connPointsFixed(): Record` - Returns fixed
+ connection points.
+
+- **`BrickModelArgument`**: Extends `BrickModel` for bricks that act as arguments.
+ - **Abstract Methods:**
+ - `get connPointsFixed(): Record<'argOutgoing', { extent: TExtent; coords: TCoords }>` - Returns
+ the outgoing connection point for arguments.
+
+- **`BrickModelInstruction`**: Extends `BrickModel` for instruction bricks.
+ - **Properties:**
+ - `connectAbove: boolean` - Indicates if the brick can connect above.
+ - `connectBelow: boolean` - Indicates if the brick can connect below.
+ - `args: { id: string; label: string }[]` - List of arguments.
+ - **Abstract Methods:**
+ - `get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } }` - Returns connection
+ points for arguments.
+ - `setBoundingBoxArg(id: string, extent: TExtent): void` - Sets the bounding box for an argument.
+
+- **`BrickModelData`**: Extends `BrickModelArgument` for data bricks.
+ - **Properties:**
+ - `dynamic: boolean` - Indicates if the data brick is dynamic.
+ - `value?: boolean | number | string` - Value of the data brick.
+ - `input?: 'boolean' | 'number' | 'string' | 'options'` - Type of input for the data brick.
+ - **Abstract Methods:**
+ - `get renderProps(): TBrickRenderPropsData` - Returns properties required to render the data brick.
+
+- **`BrickModelExpression`**: Extends `BrickModelArgument` for expression bricks.
+ - **Properties:**
+ - `args: { id: string; label: string }[]` - List of arguments.
+ - **Abstract Methods:**
+ - `get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } }` - Returns connection
+ points for arguments.
+ - `setBoundingBoxArg(id: string, extent: TExtent): void` - Sets the bounding box for an argument.
+ - `get renderProps(): TBrickRenderPropsExpression` - Returns properties required to render the
+ expression brick.
+
+- **`BrickModelStatement`**: Extends `BrickModelInstruction` for statement bricks.
+ - **Abstract Methods:**
+ - `get connPointsFixed(): Record<'insTop' | 'insBottom', { extent: TExtent; coords: TCoords }>` -
+ Returns fixed connection points for insertion points.
+ - `get renderProps(): TBrickRenderPropsStatement` - Returns properties required to render the
+ statement brick.
+
+- **`BrickModelBlock`**: Extends `BrickModelInstruction` for block bricks.
+ - **Properties:**
+ - `folded: boolean` - Indicates if the block brick is folded.
+ - **Abstract Methods:**
+ - `get connPointsFixed():
+ Record<'insTop' | 'insBottom' | 'insNest', { extent: TExtent; coords: TCoords }>` - Returns fixed
+ connection points for block insertion.
+ - `get renderProps(): TBrickRenderPropsBlock` - Returns properties required to render the block brick.
+ - `setBoundingBoxNest(extent: TExtent): void` - Sets the bounding box for the nested elements.
+
+## 2. **Concrete Classes**
+
+The concrete classes provide specific implementations of the abstract classes. These classes are used
+to create actual brick instances.
+
+- **`BrickBlock.ts`**: Concrete implementation of `BrickModelBlock`.
+ - Provides methods to calculate and return bounding boxes (`boundingBox`, `connPointsFixed`), render
+ properties (`renderProps`), and set bounding boxes (`setBoundingBoxArg`, `setBoundingBoxNest`).
+
+- **`BrickData.ts`**: Concrete implementation of `BrickModelData`.
+ - Implements properties for dynamic data (`dynamic`, `value`, `input`), and methods to get connection
+ points (`connPointsFixed`), render properties (`renderProps`), and set various properties (`setDynamic`,
+ `setValue`, `setInput`).
+
+- **`BrickExpression.ts`**: Concrete implementation of `BrickModelExpression`.
+ - Manages expressions with arguments, implementing methods to calculate argument connection points
+ (`connPointsArg`), set bounding boxes (`setBoundingBoxArg`), and provide rendering properties (`renderProps`).
+
+- **`BrickStatement.ts`**: Concrete implementation of `BrickModelStatement`.
+ - Converts argument objects to arrays, provides methods for fixed and argument connection points
+ (`connPointsFixed`, `connPointsArg`), and rendering properties (`renderProps`).
+
+## 3. **`BrickFactory.ts`: Factory Functions and Warehouse**
+
+This file manages the creation of brick instances and their storage in a warehouse for easy retrieval,
+addition, and deletion.
+
+- **Warehouse**: A `Map` to store brick instances keyed by their `uuid`.
+ - Functions:
+ - `addBrickToWarehouse(brick)`: Adds a brick instance to the warehouse.
+ - `getBrickFromWarehouse(id)`: Retrieves a brick by its ID.
+ - `deleteBrickFromWarehouse(id)`: Deletes a brick by its ID.
+
+- **Factory Functions**:
+ - `createBrickBlock`, `createBrickData`, `createBrickExpression`, `createBrickStatement`: Functions
+ that create instances of respective brick types and add them to the warehouse.
+
+## 4. **Components**
+
+Each brick type has a corresponding React component to handle its visual representation. The components
+use the `renderProps()` method from the concrete classes to render the brick according to its properties.
+
+- **`BrickWrapper.tsx`**: A Higher-Order Component (HOC) that wraps individual brick components.
+ - Retrieves brick instances from the warehouse using factory functions.
+ - Passes the `renderProps()` from each brick instance to the specific brick component (`BrickBlock`,
+ `BrickData`, `BrickExpression`, `BrickStatement`) for rendering.
+
+## 5. **Stories**
+
+- **Storybook Files**: Used to create stories for each brick type. These files demonstrate different
+states and variations of the bricks, using the factory functions to create instances for visualization.
diff --git a/modules/masonry/docs/technical-specification/Stack.md b/modules/masonry/docs/technical-specification/Stack.md
new file mode 100644
index 00000000..093125a2
--- /dev/null
+++ b/modules/masonry/docs/technical-specification/Stack.md
@@ -0,0 +1,165 @@
+# Brick Stack
+
+## 1. **Config for Brick Stack**
+
+### (A) Define the Tree Structure Representing a Brick Stack
+
+- **Tree Structure**: A hierarchical representation where each node is a brick.
+ - **Nodes**: Represent bricks with unique identifiers (`id`) that link back to their corresponding
+ brick model instances.
+ - **Chaining of Arguments**: Some bricks will have arguments that connect in a chain-like manner.
+ - **Nesting of Instructions**: Instructions can be nested within other instructions.
+ - **Shadow Instructions and Arguments**: Handle bricks that act as shadows or placeholders.
+
+### (B) Define the Properties Required to Render the Brick Stack
+
+- **Positioning Information**: Store the relative positions of each brick in the stack to determine
+how they are rendered visually.
+- **Stack Dimensions**: Overall dimensions based on the size of all bricks combined.
+
+### (C) Define How Connection Points Will Be Mapped
+
+- **Mapping Connection Points**: For any given connection point, identify:
+ - Which brick it belongs to.
+ - Which part of the brick it represents (e.g., if it's an argument connector, specify which argument).
+- **Coordinate Storage**: Store coordinates relative to the origin of the brick stack.
+
+## 2. **Code for Brick Stack**
+
+### **Brick Stack Class (Model)**
+
+This class will encapsulate the functionalities required to manage and render the brick stack. It will
+have the following:
+
+- **Methods to Set and Get the Current Tree**:
+ - Manage the current state of the tree structure representing the stack.
+
+- **Methods to Query the Map of Connection Points**:
+ - Provide a way to retrieve all connection points and their corresponding bricks.
+
+- **Method to Return Props for the React Component (View)**:
+ - Gather all properties needed to render the brick stack in the React component based on the current
+ state.
+
+### **Brick Stack Positioning Calculations**
+
+The class will also handle:
+
+- **Preorder Traversal for Argument Chains**:
+ - Query brick classes (model) to get their bounding boxes.
+ - Calculate relative positions of bricks along the argument chains.
+ - Determine sizes for argument bricks at different levels of the stack.
+
+- **Preorder Traversal for Instruction Tree**:
+ - Query brick models to get their bounding boxes.
+ - Calculate relative positions of bricks within the instruction tree.
+ - Determine sizes for instruction bricks at different levels of nesting.
+
+- **Mapping of Connection Point Coordinates**:
+ - Translate the connection points' coordinates from the brick models to their positions in the stack.
+
+### **React Component for Brick Stack (View)**
+
+- **Props will be (B)**:
+ - The component will receive props that include the positioning data and information necessary to
+ render the stack correctly.
+
+- **Use the Brick React Components Developed Earlier**:
+ - It will utilize the individual brick components (`BrickBlock`, `BrickData`, etc.) that were
+ developed earlier.
+
+### **Warehouse Module for Brick Stacks**
+
+- **Map of Brick Stack ID to Instance**:
+ - Manage instances of brick stacks using a map structure to keep track of them.
+
+- **Functions to Add, Retrieve, and Delete Instances**:
+ - Add a new brick stack, retrieve an existing one, or delete a stack from the warehouse.
+
+## 3. **Storybook Files for Brick Stack Configurations and States**
+
+### **Configurations to Cover:**
+
+- No nesting
+- Some nesting
+- Deep nesting
+- No arguments
+- Chain of arguments
+- Some missing arguments
+- Shadow arguments
+- Shadow instructions
+
+## Additional Details and Notes
+
+- **Inline and In-File Documentation**:
+ - Ensure that all classes, methods, and complex logic are well-documented to help future contributors
+ understand the codebase.
+
+- **Export the Brick Stack Class, React Component, and Warehouse Module**:
+ - Make sure these are accessible to the workspace submodule, which will utilize them.
+
+---
+
+What is required of brick
+
+## 1. **Brick Model Classes and Instances**
+
+- **Access to Concrete Classes (`BrickBlock`, `BrickData`, `BrickExpression`, `BrickStatement`)**:
+ - Instantiate these classes based on their constructor arguments.
+ - Call methods from these classes to get properties like bounding boxes, connection points, and
+ render props.
+
+- **Methods to Retrieve Brick States**:
+ - Functions like `getBoundingBox()`, `getConnPointsFixed()`, and `getConnPointsArg()` from each
+ concrete class to calculate the position and alignment of each brick in the stack.
+
+- **Methods to Set and Update Brick States**:
+ - Methods like `setScale()`, `setHighlighted()`, and other setters that allow changing the state of
+ bricks dynamically as the stack is manipulated.
+
+## 2. **Brick Factory and Warehouse Functions**
+
+- **Factory Functions** (`createBrickBlock`, `createBrickData`, etc.):
+ - These will be used to create new brick instances with the correct properties. The factory functions
+ will handle generating UUIDs and initializing instances with the correct configuration.
+
+- **Warehouse Functions** (`addBrickToWarehouse`, `getBrickFromWarehouse`, `deleteBrickFromWarehouse`):
+ - To store, retrieve, and manage instances of bricks efficiently. The brick stack needs to interact
+ with the warehouse to maintain a map of all brick instances it contains.
+
+## 3. **Type Definitions and Interfaces**
+
+- **Types for Brick Properties and States** (`TBrickRenderProps`, `TColor`, `TExtent`, `TCoords`, etc.):
+ - Use these types to ensure consistent typing for the properties and states across both the brick
+ stack and brick submodules.
+
+- **Interfaces for Brick Contracts** (`IBrick`, `IBrickBlock`, `IBrickData`, etc.):
+ - These will define the contract that each brick type must fulfill. The stack will rely on these
+ contracts to interact with different brick instances.
+
+## 4. **Render Properties and Methods**
+
+- **Render Prop Methods** (`renderProps()`):
+ - Use the methods from the concrete brick classes to gather all necessary data for rendering each
+ brick in the stack. This data will be passed to the React component representing the stack.
+
+## 5. **Bounding Box and Connection Point Calculations**
+
+- **Bounding Box Methods** (`getBoundingBox()`, etc.):
+ - Need these to determine the dimensions of each brick and calculate their positions within the stack.
+
+- **Connection Point Methods** (`getConnPointsFixed()`, `getConnPointsArg()`, etc.):
+ - These will help manage how bricks connect to each other, especially when considering nested or
+ chained configurations.
+
+## 6. **Hooks for State Changes**
+
+- **State Management Hooks**:
+ - Hooks or methods that listen to or trigger changes in brick states (e.g., highlight state, scale
+ state) so that the stack can re-render or adjust positioning when necessary.
+
+## 7. **Storybook and Test Utilities**
+
+- **Storybook Stories and Test Configurations**:
+ - Use the existing stories and test configurations to verify that the stack integrates correctly
+ with different brick types and configurations.
diff --git a/modules/masonry/docs/technical-specification/Techspec.md b/modules/masonry/docs/technical-specification/Techspec.md
new file mode 100644
index 00000000..0250242a
--- /dev/null
+++ b/modules/masonry/docs/technical-specification/Techspec.md
@@ -0,0 +1,164 @@
+# Masonry Framework
+
+## Palette
+
+### Config (Inputs)
+
+- **Categories**:
+ - **Attributes**: names, icons
+ - **Sections**:
+ - **Attributes**: name, icon, color
+ - **Bricks**:
+ - **Attributes**: id, name, description, thumbnail, BBox
+
+### Search
+
+- **Search text**:
+ - Substring match on name
+ - Substring match on description
+ - Minimum 3 characters
+
+### Drag & Drop
+
+- **For a brick**:
+ 1. Start drag operation
+ 2. Transfer brick's logo to workspace
+ 3. Replace thumbnail with silhouette (silhouette is a grayed-out thumbnail at 50% opacity)
+
+- **Drag Ended**:
+ - Replace silhouette with the original thumbnail
+
+- **Workspace (WS)**:
+ - Handles drag operations
+ - Stops drag within the palette
+ - Stops drag outside the palette
+
+## Brick
+
+### Brick Types
+
+- **Data Bricks**
+ - Returns values
+ - Connects only as arguments
+ - Example: input
+- **Expression Bricks**
+ - Returns values
+ - Can take 0 or more arguments
+ - Connects only as arguments
+- **Statement Bricks**
+ - Execute actions
+ - Connects with other statement/block types
+ - Can take 0 or more arguments
+- **Block Bricks**
+ - Can nest other bricks
+ - Connects with other statement/block types
+ - Can take 0 or more arguments
+
+### Brick Appearance
+
+- **Attributes**:
+ - Color
+ - Shape
+ - Primary label
+ - Any labels (copied)
+ - Any connectors
+
+### Brick Interactions
+
+- **Inline Edit**:
+ - Only applicable to Data bricks
+ - Example: input text
+
+### Brick Structure Overview
+
+- **Brick**:
+ - Color
+ - Argument connectors
+ - Connectors for expression
+- **Argument**:
+ - Can connect to data bricks
+- **Data**:
+ - Inputs, labels, nodes
+- **Expression**:
+ - Contains data bricks
+- **Statement**:
+ - Instructions, connected to block
+- **Block**:
+ - Can nest other bricks
+
+## Stack
+
+### Stack Validation
+
+- **Visual Feedback**:
+ - Indicators show whether brick combinations are valid.
+ - Example: A green outline appears for a valid connection, while a red outline indicates an
+ invalid connection.
+- **Error Indicators**:
+ - Provide explanations for incompatible connections to help users troubleshoot.
+ - Example: A reddish boundary and error message explain why the connection is invalid.
+
+### Stack Editing
+
+- **Connection Editing**:
+ - Options for repositioning, disconnecting, or connecting bricks.
+ - Visual indicators show possible connections.
+ - Example: Highlight potential connection points when a brick is moved.
+
+### Stack Grouping
+
+- **Collapsible Groups**:
+ - Groups can be collapsed or expanded to manage complexity.
+ - Example: Users can organize bricks into groups that can be collapsed to reduce visual clutter.
+ - Collapsed groups can be moved as a single unit.
+
+## Workspace
+
+### Layout and Interaction
+
+- **Cloning**:
+ - Users can easily create copies of bricks for repeated use.
+ - Example: Right-click on a brick and select "Duplicate".
+- **Scaling and Rotation**:
+ - Bricks can be resized and rotated to fit the workspace better.
+ - Example: Click and drag corner handles to resize, or use rotation handles to rotate.
+- **Undo/Redo**:
+ - Users can revert or reapply changes to their stacks.
+ - Example: Undo the last action with Ctrl+Z, redo with Ctrl+Y.
+- **Deletion of Bricks**:
+ - Users can remove or delete bricks.
+ - Example: Drag a brick to the trash icon or press the delete key.
+
+### Data Flow and Interaction
+
+#### Adding Bricks
+
+- **From Palette to Workspace**:
+ 1. User drags a brick from the palette.
+ 2. Workspace detects the drag operation and shows a drop area.
+ 3. User drops the brick in the workspace.
+ 4. Workspace creates a new brick instance at the drop location.
+
+#### Connecting Bricks
+
+- **Within Workspace**:
+ 1. User selects a connection point on a brick.
+ 2. User drags to another brick's connection point.
+ 3. Workspace validates the connection.
+ 4. If valid, a connection is established and visual feedback is provided.
+ 5. If invalid, an error message is shown.
+
+#### Editing Connections
+
+- **Repositioning and Disconnecting**:
+ 1. User clicks on a connection line.
+ 2. User can drag the connection to a new point or disconnect it.
+ 3. Workspace updates the connections and provides visual feedback.
+
+#### Grouping Bricks
+
+- **Creating and Managing Groups**:
+ 1. User selects multiple bricks.
+ 2. User groups them into a collapsible unit.
+ 3. Workspace treats the group as a single entity for movement and scaling.
+ 4. User can collapse or expand the group to manage workspace complexity.
diff --git a/modules/masonry/package.json b/modules/masonry/package.json
new file mode 100644
index 00000000..ff6aa6d5
--- /dev/null
+++ b/modules/masonry/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@sugarlabs/mb4-module-masonry",
+ "version": "4.2.0",
+ "description": "Graphical project builder using drag & drop code bricks",
+ "private": "true",
+ "main": "src/index.ts",
+ "scripts": {
+ "test": "vitest",
+ "coverage": "vitest run --coverage",
+ "lint": "eslint src",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build",
+ "playground": "vite playground --host --port 5601"
+ },
+ "dependencies": {
+ "uuid": "^10.0.0"
+ },
+ "devDependencies": {
+ "@types/uuid": "^10.0.0"
+ }
+}
diff --git a/modules/masonry/playground/index.html b/modules/masonry/playground/index.html
new file mode 100644
index 00000000..85dbc925
--- /dev/null
+++ b/modules/masonry/playground/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ Masonry - Playground
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/masonry/playground/index.tsx b/modules/masonry/playground/index.tsx
new file mode 100644
index 00000000..184a0b11
--- /dev/null
+++ b/modules/masonry/playground/index.tsx
@@ -0,0 +1,18 @@
+import { createRoot } from 'react-dom/client';
+import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
+
+import WorkSpace from './pages/workspace';
+
+const router = createBrowserRouter([
+ {
+ path: '/workspace',
+ element: ,
+ },
+ {
+ path: '/',
+ element: ,
+ },
+]);
+
+const root = createRoot(document.getElementById('playground-root') as HTMLElement);
+root.render();
diff --git a/modules/masonry/playground/pages/workspace/BrickBlock.tsx b/modules/masonry/playground/pages/workspace/BrickBlock.tsx
new file mode 100644
index 00000000..23b35742
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BrickBlock.tsx
@@ -0,0 +1,36 @@
+import type { DOMAttributes, JSX } from 'react';
+import type { Brick } from './data';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function ({
+ brickData,
+ moveProps,
+ coords,
+ color,
+}: {
+ brickData: Brick;
+ moveProps: DOMAttributes;
+ coords: { x: number; y: number };
+ color: string;
+}): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/modules/masonry/playground/pages/workspace/BrickData.tsx b/modules/masonry/playground/pages/workspace/BrickData.tsx
new file mode 100644
index 00000000..23b35742
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BrickData.tsx
@@ -0,0 +1,36 @@
+import type { DOMAttributes, JSX } from 'react';
+import type { Brick } from './data';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function ({
+ brickData,
+ moveProps,
+ coords,
+ color,
+}: {
+ brickData: Brick;
+ moveProps: DOMAttributes;
+ coords: { x: number; y: number };
+ color: string;
+}): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/modules/masonry/playground/pages/workspace/BrickExpression.tsx b/modules/masonry/playground/pages/workspace/BrickExpression.tsx
new file mode 100644
index 00000000..50be6868
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BrickExpression.tsx
@@ -0,0 +1,34 @@
+import type { DOMAttributes, JSX } from 'react';
+import type { Brick } from './data';
+
+export default function ({
+ brickData,
+ moveProps,
+ coords,
+ color,
+}: {
+ brickData: Brick;
+ moveProps: DOMAttributes;
+ coords: { x: number; y: number };
+ color: string;
+}): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/modules/masonry/playground/pages/workspace/BrickFactory.tsx b/modules/masonry/playground/pages/workspace/BrickFactory.tsx
new file mode 100644
index 00000000..cc541a14
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BrickFactory.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { useMove } from 'react-aria';
+import BrickBlock from './BrickBlock';
+import BrickExpression from './BrickExpression';
+import BrickStatement from './BrickStatement';
+import BrickData from './BrickData';
+import { useBricksCoords } from './BricksCoordsStore';
+import { WORKSPACES_DATA } from './data';
+import type { Brick } from './data';
+import { getBelowBricksIds } from './utils';
+
+const BrickFactory = ({
+ brickData,
+ isPalette = false,
+ onDragEnd,
+}: {
+ brickData: Brick;
+ isPalette?: boolean;
+ onDragEnd?: (brick: Brick) => void;
+}) => {
+ const CONTAINER_SIZE_X = 2000;
+ const CONTAINER_SIZE_Y = 800;
+ const BRICK_HEIGHT = brickData.instance.bBoxBrick.extent.height;
+ const BRICK_WIDTH = brickData.instance.bBoxBrick.extent.width;
+ const { getCoords, setCoords } = useBricksCoords();
+ const brickCoords = getCoords(brickData.id) || { x: 0, y: 0 };
+ const [color, setColor] = useState(brickData.instance.colorBg as string);
+
+ const clampX = (pos: number) => Math.min(Math.max(pos, 0), CONTAINER_SIZE_X - BRICK_WIDTH * 2);
+ const clampY = (pos: number) => Math.min(Math.max(pos, 0), CONTAINER_SIZE_Y - BRICK_HEIGHT * 2);
+
+ const { moveProps } = useMove({
+ onMoveStart(e) {
+ console.log(`move start with pointerType = ${e.pointerType}`);
+ setColor('white');
+ },
+ onMove(e) {
+ if (!isPalette) {
+ const newX = brickCoords.x + e.deltaX;
+ const newY = brickCoords.y + e.deltaY;
+ setCoords(brickData.id, { x: clampX(newX), y: clampY(newY) });
+
+ brickData.childBricks.forEach((childBrick) => {
+ const childBrickCoords = getCoords(childBrick)!;
+ setCoords(childBrick, {
+ x: childBrickCoords.x + e.deltaX,
+ y: childBrickCoords.y + e.deltaY,
+ });
+ });
+
+ const belowBrickIds = getBelowBricksIds(WORKSPACES_DATA[0].data, brickData.id);
+ belowBrickIds.forEach((belowBrickId) => {
+ const belowBrickCoords = getCoords(belowBrickId)!;
+ setCoords(belowBrickId, {
+ x: belowBrickCoords.x + e.deltaX,
+ y: belowBrickCoords.y + e.deltaY,
+ });
+ });
+ }
+ },
+ onMoveEnd(e) {
+ console.log(`move end with pointerType = ${e.pointerType}`);
+ setColor(brickData.instance.colorBg as string);
+ if (isPalette && onDragEnd) {
+ onDragEnd(brickData);
+ }
+ },
+ });
+
+ const BrickComponent = {
+ data: BrickData,
+ expression: BrickExpression,
+ statement: BrickStatement,
+ block: BrickBlock,
+ }[brickData.type];
+
+ return (
+
+ );
+};
+
+export default BrickFactory;
diff --git a/modules/masonry/playground/pages/workspace/BrickStatement.tsx b/modules/masonry/playground/pages/workspace/BrickStatement.tsx
new file mode 100644
index 00000000..50be6868
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BrickStatement.tsx
@@ -0,0 +1,34 @@
+import type { DOMAttributes, JSX } from 'react';
+import type { Brick } from './data';
+
+export default function ({
+ brickData,
+ moveProps,
+ coords,
+ color,
+}: {
+ brickData: Brick;
+ moveProps: DOMAttributes;
+ coords: { x: number; y: number };
+ color: string;
+}): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/modules/masonry/playground/pages/workspace/BricksCoordsStore.ts b/modules/masonry/playground/pages/workspace/BricksCoordsStore.ts
new file mode 100644
index 00000000..94d3b73e
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/BricksCoordsStore.ts
@@ -0,0 +1,50 @@
+import { create } from 'zustand';
+
+type CoordsState = {
+ allCoords: {
+ brickId: string;
+ coords: {
+ x: number;
+ y: number;
+ };
+ }[];
+ setCoords: (brickId: string, coords: { x: number; y: number }) => void;
+ getCoords: (brickId: string) => { x: number; y: number } | undefined;
+};
+
+const useBricksCoordsStore = create((set, get) => ({
+ allCoords: [
+ { brickId: '1', coords: { x: 50, y: 50 } },
+ { brickId: '2', coords: { x: 68, y: 92 } },
+ { brickId: '3', coords: { x: 68, y: 134 } },
+ { brickId: '4', coords: { x: 68, y: 176 } },
+ { brickId: '5', coords: { x: 86, y: 218 } },
+ { brickId: '6', coords: { x: 68, y: 302 } },
+ ],
+ setCoords: (brickId: string, coords: { x: number; y: number }) =>
+ set(
+ (state: {
+ allCoords: {
+ brickId: string;
+ coords: {
+ x: number;
+ y: number;
+ };
+ }[];
+ }) => ({
+ allCoords: state.allCoords.map((item) =>
+ item.brickId === brickId ? { brickId, coords } : item,
+ ),
+ }),
+ ),
+ getCoords: (brickId: string) =>
+ get().allCoords.find((item) => item.brickId === brickId)?.coords,
+}));
+
+export const useBricksCoords = () => {
+ const allCoords = useBricksCoordsStore((state) => state.allCoords);
+ const setCoords = useBricksCoordsStore((state) => state.setCoords);
+ const getCoords = useBricksCoordsStore((state) => state.getCoords);
+
+ return { allCoords, setCoords, getCoords };
+};
diff --git a/modules/masonry/playground/pages/workspace/data.ts b/modules/masonry/playground/pages/workspace/data.ts
new file mode 100644
index 00000000..4acfd119
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/data.ts
@@ -0,0 +1,227 @@
+import {
+ ModelBrickBlock,
+ ModelBrickData,
+ ModelBrickExpression,
+ ModelBrickStatement,
+} from '@/brick';
+import type { TBrickType, TBrickCoords, TBrickArgDataType } from '@/@types/brick';
+
+export 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[] }[] = [
+ {
+ id: 'workspace1',
+ data: [
+ {
+ id: '1',
+ type: 'block',
+ instance: new ModelBrickBlock({
+ label: 'Block',
+ args: Object.fromEntries(
+ [].map<
+ [string, { label: string; dataType: TBrickArgDataType; meta: unknown }]
+ >((name) => [name, { label: name, dataType: 'any', meta: undefined }]),
+ ),
+ colorBg: 'lightpink',
+ colorFg: 'black',
+ outline: 'deeppink',
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ nestLengthY: 125,
+ }),
+ surroundingBricks: { above: '', below: '' },
+ childBricks: ['2', '3', '4', '5', '6'],
+ coords: { x: 50, y: 50 },
+ children: [
+ {
+ id: '2',
+ type: 'statement',
+ instance: new ModelBrickStatement({
+ label: 'Statement',
+ args: Object.fromEntries(
+ [].map<
+ [
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ },
+ ]
+ >((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg: 'lightblue',
+ colorFg: 'black',
+ outline: 'blue',
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ }),
+ surroundingBricks: { above: '1', below: '3' },
+ childBricks: [],
+ coords: { x: 68, y: 92 },
+ },
+ {
+ id: '3',
+ type: 'statement',
+ instance: new ModelBrickStatement({
+ label: 'Statement',
+ args: Object.fromEntries(
+ [].map<
+ [
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ },
+ ]
+ >((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg: 'lightgreen',
+ colorFg: 'black',
+ outline: 'green',
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ }),
+ surroundingBricks: { above: '2', below: '4' },
+ childBricks: [],
+ coords: { x: 68, y: 134 },
+ },
+ {
+ id: '4',
+ type: 'block',
+ instance: new ModelBrickBlock({
+ label: 'Block',
+ args: Object.fromEntries(
+ [].map<
+ [
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ },
+ ]
+ >((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg: 'brown',
+ colorFg: 'black',
+ outline: 'grey',
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ nestLengthY: 17,
+ }),
+ surroundingBricks: { above: '3', below: '6' },
+ childBricks: ['5'],
+ coords: { x: 68, y: 176 },
+ children: [
+ {
+ id: '5',
+ type: 'statement',
+ instance: new ModelBrickStatement({
+ label: 'Statement',
+ args: Object.fromEntries(
+ [].map<
+ [
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ },
+ ]
+ >((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg: 'orange',
+ colorFg: 'black',
+ outline: 'red',
+
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ }),
+ surroundingBricks: { above: '', below: '' },
+ childBricks: [],
+ coords: { x: 86, y: 218 },
+ },
+ ],
+ },
+ {
+ id: '6',
+ type: 'statement',
+ instance: new ModelBrickStatement({
+ label: 'Statement',
+ args: Object.fromEntries(
+ [].map<
+ [
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ },
+ ]
+ >((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg: 'lightgreen',
+ colorFg: 'black',
+ outline: 'green',
+ scale: 2,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ }),
+ surroundingBricks: { above: '4', below: '' },
+ childBricks: [],
+ coords: { x: 68, y: 302 },
+ },
+ ],
+ },
+ ],
+ },
+];
diff --git a/modules/masonry/playground/pages/workspace/index.tsx b/modules/masonry/playground/pages/workspace/index.tsx
new file mode 100644
index 00000000..213afe30
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/index.tsx
@@ -0,0 +1,37 @@
+import BrickFactory from './BrickFactory';
+import { WORKSPACES_DATA } from './data';
+import type { Brick } from './data';
+
+function RenderBricks({ brickData }: { brickData: Brick }) {
+ return (
+ <>
+
+ {brickData.children &&
+ brickData.children?.length > 0 &&
+ brickData.children.map((child) => )}
+ >
+ );
+}
+
+function WorkSpace() {
+ return (
+
+ {WORKSPACES_DATA.map((workspace) => (
+
+ ))}
+
+ );
+}
+
+export default WorkSpace;
diff --git a/modules/masonry/playground/pages/workspace/utils.ts b/modules/masonry/playground/pages/workspace/utils.ts
new file mode 100644
index 00000000..8adb28d5
--- /dev/null
+++ b/modules/masonry/playground/pages/workspace/utils.ts
@@ -0,0 +1,23 @@
+import type { Brick } from './data';
+
+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;
+}
diff --git a/modules/masonry/playground/vite.config.ts b/modules/masonry/playground/vite.config.ts
new file mode 100644
index 00000000..abd1d975
--- /dev/null
+++ b/modules/masonry/playground/vite.config.ts
@@ -0,0 +1,18 @@
+import path from 'path';
+
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [
+ //
+ react(),
+ ],
+
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '..', 'src'),
+ },
+ extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'],
+ },
+});
diff --git a/modules/masonry/src/@types/brick.d.ts b/modules/masonry/src/@types/brick.d.ts
new file mode 100644
index 00000000..8488cbe8
--- /dev/null
+++ b/modules/masonry/src/@types/brick.d.ts
@@ -0,0 +1,205 @@
+/**
+ * @type
+ * Kind (instruction or argument) of a brick.
+ */
+export type TBrickKind = 'instruction' | 'argument';
+
+/**
+ * @type
+ * Type (data, expression, statement, block) of a brick.
+ */
+export type TBrickType = 'data' | 'expression' | 'statement' | 'block';
+
+/**
+ * @type
+ * Bounding box dimensions of a brick.
+ */
+export type TExtent = {
+ width: number;
+ height: number;
+};
+
+/**
+ * @type
+ * Position co-ordinates of a brick.
+ */
+export type TCoords = {
+ /** relative x co-ordinate */
+ x: number;
+ /** relative y co-ordinate */
+ y: number;
+};
+
+/**
+ * @type
+ * Defines color property of a brick. Supported types are RGB, HSL, and hexadecimal.
+ */
+export type TColor = ['rgb' | 'hsl', number, number, number] | string;
+
+// -------------------------------------------------------------------------------------------------
+
+type TBrickRenderProps = {
+ path: string;
+ label: string;
+ glyph: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ outline: TColor;
+ scale: number;
+};
+
+export type TBrickRenderPropsData = TBrickRenderProps & {
+ // reserving spot for future-proofing
+};
+
+export type TBrickRenderPropsExpression = TBrickRenderProps & {
+ labelArgs: string[];
+ boundingBoxArgs: TExtent[];
+};
+
+export type TBrickRenderPropsStatement = TBrickRenderProps & {
+ labelArgs: string[];
+ boundingBoxArgs: TExtent[];
+};
+
+export type TBrickRenderPropsBlock = TBrickRenderProps & {
+ labelArgs: string[];
+ boundingBoxArgs: TExtent[];
+ boundingBoxNest: TExtent;
+ folded: boolean;
+};
+
+// -------------------------------------------------------------------------------------------------
+
+/**
+ * @interface
+ * Arguments for a brick.
+ */
+export interface IBrickArgs {
+ /** list of argument connection points of the brick */
+ get connPointsArg(): {
+ [id: string]: {
+ /** bounding box dimensions of the connection point */
+ extent: TExtent;
+ /** co-ordinates of the connection point */
+ coords: TCoords;
+ };
+ };
+
+ /**
+ * Sets the bounding box extents for an arg
+ * @param id arg identifier
+ * @param extent width and height values of the arg
+ */
+ setBoundingBoxArg(id: string, extent: TExtent): void;
+}
+
+/**
+ * @interface
+ * Type definition of a generic brick (any type).
+ */
+export interface IBrick {
+ /** unique ID of the brick */
+ get uuid(): string;
+ /** name of the brick — to be used for internal bookkeeping */
+ get name(): string;
+ /** kind of the brick */
+ get kind(): TBrickKind;
+ /** type of the brick */
+ get type(): TBrickType;
+
+ /** whether brick is highlighted */
+ set highlighted(value: boolean);
+ /** current vector scale factor */
+ set scale(value: number);
+
+ /** bounding box dimensions of the brick */
+ get boundingBox(): TExtent;
+ /** list of fixed connection points of the brick */
+ get connPointsFixed(): Record<
+ string,
+ {
+ /** bounding box dimensions of the connection point */
+ extent: TExtent;
+ /** co-ordinates of the connection point */
+ coords: TCoords;
+ }
+ >;
+}
+
+/**
+ * @interface
+ * Type definition of a generic argument brick (data or expression type).
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface IBrickArgument extends IBrick {
+ // reserving spot for future-proofing
+}
+
+/**
+ * @interface
+ * Type definition of a generic instruction brick (statement or block type).
+ */
+export interface IBrickInstruction extends IBrick, IBrickArgs {
+ /** is connection allowed above the brick */
+ get connectAbove(): boolean;
+ /** is connection allowed below the brick */
+ get connectBelow(): boolean;
+}
+
+/**
+ * @interface
+ * Type definition of a data brick.
+ */
+export interface IBrickData extends IBrickArgument {
+ /** whether brick has a static label or value can be updated */
+ get dynamic(): boolean;
+ /** (if dynamic) current value of the brick */
+ get value(): undefined | boolean | number | string;
+ /** (if dynamic) input mode for the brick (checkbox, number box, text box, dropdown, etc.) */
+ get input(): undefined | 'boolean' | 'number' | 'string' | 'options';
+
+ /** list of properties required to render the data brick graphic */
+ get renderProps(): IBrickRenderPropsData;
+}
+
+/**
+ * @interface
+ * Type definition of an argument brick.
+ */
+export interface IBrickExpression extends IBrickArgument, IBrickArgs, IBrickArgsState {
+ /** list of properties required to render the expression brick graphic */
+ get renderProps(): IBrickRenderPropsExpression;
+}
+
+/**
+ * @interface
+ * Type definition of a statement brick.
+ */
+export interface IBrickStatement extends IBrickInstruction {
+ /** list of properties required to render the statement brick graphic */
+ get renderProps(): IBrickRenderPropsStatement;
+}
+
+/**
+ * @interface
+ * Type definition of a block brick.
+ */
+export interface IBrickBlock extends IBrickInstruction, IBrickNotchInsNestTopState {
+ /** whether brick nesting is hidden */
+ set folded(value: boolean);
+
+ /** list of properties required to render the block brick graphic */
+ get renderProps(): IBrickRenderPropsBlock;
+
+ /**
+ * Sets the bounding box extents for the nested area
+ * @param extent width and height values of the nest area
+ */
+ setBoundingBoxNest(extent: TExtent): void;
+
+ get connPointsFixed(): Record<
+ 'insTop' | 'insBottom' | 'insNest',
+ { extent: TExtent; coords: TCoords }
+ >;
+}
diff --git a/modules/masonry/src/brick/README.md b/modules/masonry/src/brick/README.md
new file mode 100644
index 00000000..d5830cef
--- /dev/null
+++ b/modules/masonry/src/brick/README.md
@@ -0,0 +1,121 @@
+# Data Model for Bricks
+
+## Data Bricks
+
+### Intrinsic
+
+- `uuid`: unique ID for the brick instance
+- `name`: name for internal bookkeeping
+- `kind`: argument or instruction
+- `type`: data, expression, statement, block
+- `label`: display name
+- `glyph`: glyph icon
+- `dataType`: the type (boolean, number, string, any) of value returned
+- `dynamic`: whether label is fixed or is a modifiable value
+- `value`: modifiable value
+- `input`: modification input (checkbox, number, text box, list of options)
+
+### Style
+
+- `colorBg`: background fill color
+- `colorFg`: text color
+- `outline`: outline/stroke color
+- `scale`: sizing scale factor
+
+### State
+
+- `highlighted`: whether brick is highlighted
+- `extent`(G): bounding box dimensions
+
+## Expression Bricks
+
+### Intrinsic
+
+- `uuid`: unique ID for the brick instance
+- `name`: name for internal bookkeeping
+- `kind`: argument or instruction
+- `type`: data, expression, statement, block
+- `label`: display name
+- `glyph`: glyph icon
+- `dataType`: type (boolean, number, string, any) of value returned
+- `args`: map of argument keys and labels, type, and metadata
+
+### Style
+
+- `colorBg`: background fill color
+- `colorFg`: text color
+- `outline`: outline/stroke color
+- `scale`: sizing scale factor
+
+### State
+
+- `highlighted`: whether brick is highlighted
+- `extent`(G): bounding box dimensions
+- `argsExtent`: bounding box dimensions for each argument
+- `argsCoords`(G): relative coordinates of each argument connection point
+
+## Statement Bricks
+
+### Intrinsic
+
+- `uuid`: unique ID for the brick instance
+- `name`: name for internal bookkeeping
+- `kind`: argument or instruction
+- `type`: data, expression, statement, block
+- `label`: display name
+- `glyph`: glyph icon
+- `args`: map of argument keys and labels, type, and metadata
+
+### Style
+
+- `colorBg`: background fill color
+- `colorFg`: text color
+- `outline`: outline/stroke color
+- `scale`: sizing scale factor
+- `connectAbove`: whether connection above brick is allowed
+- `connectAbove`: whether connection below brick is allowed
+
+### State
+
+- `highlighted`: whether brick is highlighted
+- `extent`(G): bounding box dimensions
+- `argsExtent`: bounding box dimensions for each argument
+- `argsCoords`(G): relative coordinates of each argument connection point
+
+## Block Bricks
+
+### Intrinsic
+
+- `uuid`: unique ID for the brick instance
+- `name`: name for internal bookkeeping
+- `kind`: argument or instruction
+- `type`: data, expression, statement, block
+- `label`: display name
+- `glyph`: glyph icon
+- `args`: map of argument keys and labels, type, and metadata
+
+### Style
+
+- `colorBg`: background fill color
+- `colorFg`: text color
+- `outline`: outline/stroke color
+- `scale`: sizing scale factor
+- `connectAbove`: whether connections above brick is allowed
+- `connectAbove`: whether connections below brick is allowed
+
+### State
+
+- `highlighted`: whether brick is highlighted
+- `extent`(G): bounding box dimensions
+- `argsExtent`: bounding box dimensions for each argument
+- `argsCoords`(G): relative coordinates of each argument connection point
+- `nestExtent`: bounding box dimensions of child nesting
+- `collapsed`: whether or not inner nesting is visible
+
+---
+
+**Note:** Intrinsic and Style properties are set in the constructor and cannot be modified once
+instantiated. They are accesible using getters.
+
+**Note:** States marked '(G)' represent getters — values for those will be generated within the
+instance and cannot be set from outside.
diff --git a/modules/masonry/src/brick/design0/BrickBlock.ts b/modules/masonry/src/brick/design0/BrickBlock.ts
new file mode 100644
index 00000000..ef3878e7
--- /dev/null
+++ b/modules/masonry/src/brick/design0/BrickBlock.ts
@@ -0,0 +1,122 @@
+import type { TBrickRenderPropsBlock, TColor, TCoords, TExtent } from '@/@types/brick';
+import { BrickModelBlock } from '../model';
+import { generatePath } from '../utils/path';
+
+// -------------------------------------------------------------------------------------------------
+
+/**
+ * @class
+ * Final class that defines a block brick.
+ */
+export default class BrickBlock extends BrickModelBlock {
+ readonly _pathResults: ReturnType;
+
+ private _boundingBoxArgs: Record = {};
+ private _boundingBoxNest: TExtent = { width: 0, height: 0 };
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ connectAbove: boolean;
+ connectBelow: boolean;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) {
+ super(params);
+
+ this._pathResults = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: this._connectAbove,
+ hasNotchInsBot: this._connectBelow,
+ scale: this._scale,
+ nestLengthY: this._args.length * 20, // Example of generating nest length based on argument count
+ innerLengthX: 100,
+ argHeights: Array.from({ length: this._args.length }, () => 17),
+ });
+ }
+
+ public get boundingBox(): TExtent {
+ return {
+ width: this._pathResults.bBoxBrick.extent.width,
+ height: this._pathResults.bBoxBrick.extent.height,
+ };
+ }
+
+ public get connPointsFixed(): Record<
+ 'insTop' | 'insBottom' | 'insNest',
+ { extent: TExtent; coords: TCoords }
+ > {
+ return {
+ insTop: {
+ extent: this._pathResults.bBoxNotchInsTop!.extent,
+ coords: this._pathResults.bBoxNotchInsTop!.coords,
+ },
+ insBottom: {
+ extent: this._pathResults.bBoxNotchInsBot!.extent,
+ coords: this._pathResults.bBoxNotchInsBot!.coords,
+ },
+ insNest: {
+ extent: this._pathResults.bBoxNotchInsNestTop!.extent,
+ coords: this._pathResults.bBoxNotchInsNestTop!.coords,
+ },
+ };
+ }
+
+ public get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } } {
+ const results: { [id: string]: { extent: TExtent; coords: TCoords } } = {};
+
+ this._args.forEach(({ id }, index) => {
+ results[id] = {
+ extent: { width: 10, height: 10 }, // Example extent
+ coords: { x: 0, y: index * 20 }, // Example coordinates calculation
+ };
+ });
+
+ return results;
+ }
+
+ public get renderProps(): TBrickRenderPropsBlock {
+ return {
+ path: this._pathResults.path,
+
+ label: this._label,
+ labelArgs: this._args.map(({ label }) => label),
+
+ boundingBoxArgs: this._args.map(({ id }) => this._boundingBoxArgs[id]),
+ boundingBoxNest: this._boundingBoxNest,
+
+ glyph: this._glyph,
+ colorBg: !this._highlighted ? this._colorBg : this._colorBgHighlight,
+ colorFg: !this._highlighted ? this._colorFg : this._colorFgHighlight,
+ outline: this._outline,
+ scale: this._scale,
+ folded: this.folded,
+ };
+ }
+
+ public setBoundingBoxArg(id: string, extent: TExtent): void {
+ this._boundingBoxArgs[id] = extent;
+ }
+
+ public setBoundingBoxNest(extent: TExtent): void {
+ this._boundingBoxNest = extent;
+ }
+
+ public setHighlighted(highlighted: boolean): void {
+ this._highlighted = highlighted;
+ }
+}
diff --git a/modules/masonry/src/brick/design0/BrickData.ts b/modules/masonry/src/brick/design0/BrickData.ts
new file mode 100644
index 00000000..a219da6e
--- /dev/null
+++ b/modules/masonry/src/brick/design0/BrickData.ts
@@ -0,0 +1,82 @@
+import type { TBrickRenderPropsData, TColor, TCoords, TExtent } from '@/@types/brick';
+import { BrickModelData } from '../model';
+import { generatePath } from '../utils/path';
+
+/**
+ * @class
+ * Final class that defines a data brick.
+ */
+export default class BrickData extends BrickModelData {
+ readonly _pathResults: ReturnType;
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+ label: string;
+ glyph: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ dynamic: boolean;
+ value?: boolean | number | string;
+ input?: 'boolean' | 'number' | 'string' | 'options';
+ }) {
+ super(params);
+
+ this._pathResults = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: this._scale,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ }
+
+ public get boundingBox(): TExtent {
+ return {
+ width: this._pathResults.bBoxBrick.extent.width,
+ height: this._pathResults.bBoxBrick.extent.height,
+ };
+ }
+
+ public get connPointsFixed(): Record<'argOutgoing', { extent: TExtent; coords: TCoords }> {
+ return {
+ argOutgoing: {
+ extent: this._pathResults.bBoxNotchArg!.extent,
+ coords: this._pathResults.bBoxNotchArg!.coords,
+ },
+ };
+ }
+
+ public get renderProps(): TBrickRenderPropsData {
+ return {
+ path: this._pathResults.path,
+ label: this._label,
+ glyph: this._glyph,
+ colorBg: !this._highlighted ? this._colorBg : this._colorBgHighlight,
+ colorFg: !this._highlighted ? this._colorFg : this._colorFgHighlight,
+ outline: this._outline,
+ scale: this._scale,
+ };
+ }
+
+ public setDynamic(dynamic: boolean): void {
+ this._dynamic = dynamic;
+ }
+
+ public setValue(value: boolean | number | string): void {
+ this._value = value;
+ }
+
+ public setInput(input: 'boolean' | 'number' | 'string' | 'options'): void {
+ this._input = input;
+ }
+
+ public setHighlighted(highlighted: boolean): void {
+ this._highlighted = highlighted;
+ }
+}
diff --git a/modules/masonry/src/brick/design0/BrickExpression.ts b/modules/masonry/src/brick/design0/BrickExpression.ts
new file mode 100644
index 00000000..4b9fd0e4
--- /dev/null
+++ b/modules/masonry/src/brick/design0/BrickExpression.ts
@@ -0,0 +1,89 @@
+import type { TBrickRenderPropsExpression, TColor, TCoords, TExtent } from '@/@types/brick';
+import { BrickModelExpression } from '../model';
+import { generatePath } from '../utils/path';
+
+/**
+ * @class
+ * Final class that defines an expression brick.
+ */
+export default class BrickExpression extends BrickModelExpression {
+ readonly _pathResults: ReturnType;
+
+ private _boundingBoxArgs: Record = {};
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ args: { id: string; label: string }[];
+ }) {
+ super(params);
+
+ this._pathResults = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: this._scale,
+ innerLengthX: 100,
+ argHeights: Array.from({ length: this._args.length }, () => 17),
+ });
+ }
+
+ public get boundingBox(): TExtent {
+ return {
+ width: this._pathResults.bBoxBrick.extent.width,
+ height: this._pathResults.bBoxBrick.extent.height,
+ };
+ }
+
+ public get connPointsFixed(): Record<'argOutgoing', { extent: TExtent; coords: TCoords }> {
+ return {
+ argOutgoing: {
+ extent: this._pathResults.bBoxNotchArg!.extent,
+ coords: this._pathResults.bBoxNotchArg!.coords,
+ },
+ };
+ }
+
+ public get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } } {
+ const results: { [id: string]: { extent: TExtent; coords: TCoords } } = {};
+
+ this._args.forEach(({ id }, index) => {
+ results[id] = {
+ extent: { width: 10, height: 10 }, // Example extent
+ coords: { x: 0, y: index * 20 }, // Example coordinates calculation
+ };
+ });
+
+ return results;
+ }
+
+ public get renderProps(): TBrickRenderPropsExpression {
+ return {
+ path: this._pathResults.path,
+ label: this._label,
+ labelArgs: this._args.map(({ label }) => label),
+ boundingBoxArgs: this._args.map(({ id }) => this._boundingBoxArgs[id]),
+ glyph: this._glyph,
+ colorBg: !this._highlighted ? this._colorBg : this._colorBgHighlight,
+ colorFg: !this._highlighted ? this._colorFg : this._colorFgHighlight,
+ outline: this._outline,
+ scale: this._scale,
+ };
+ }
+
+ public setBoundingBoxArg(id: string, extent: TExtent): void {
+ this._boundingBoxArgs[id] = extent;
+ }
+
+ public setHighlighted(highlighted: boolean): void {
+ this._highlighted = highlighted;
+ }
+}
diff --git a/modules/masonry/src/brick/design0/BrickStatement.ts b/modules/masonry/src/brick/design0/BrickStatement.ts
new file mode 100644
index 00000000..7dae72d6
--- /dev/null
+++ b/modules/masonry/src/brick/design0/BrickStatement.ts
@@ -0,0 +1,107 @@
+import type { TBrickRenderPropsStatement, TColor, TCoords, TExtent } from '@/@types/brick';
+import { BrickModelStatement } from '../model';
+import { generatePath } from '../utils/path';
+
+/**
+ * @class
+ * Final class that defines a statement brick.
+ */
+export default class BrickStatement extends BrickModelStatement {
+ readonly _pathResults: ReturnType;
+
+ private _boundingBoxArgs: Record = {};
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+ label: string;
+ glyph: string;
+ args: { id: string; label: string }[];
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ scale: number;
+ connectAbove: boolean;
+ connectBelow: boolean;
+ }) {
+ super(params);
+
+ this._pathResults = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: params.connectAbove,
+ hasNotchInsBot: params.connectBelow,
+ scale: this._scale,
+ innerLengthX: 100,
+ argHeights: Array.from({ length: params.args.length }, () => 17),
+ });
+ }
+
+ public get boundingBox(): TExtent {
+ return {
+ width: this._pathResults.bBoxBrick.extent.width,
+ height: this._pathResults.bBoxBrick.extent.height,
+ };
+ }
+
+ public get connPointsFixed(): Record<
+ 'insTop' | 'insBottom',
+ { extent: TExtent; coords: TCoords }
+ > {
+ return {
+ insTop: {
+ extent: this._pathResults.bBoxNotchInsTop!.extent,
+ coords: this._pathResults.bBoxNotchInsTop!.coords,
+ },
+ insBottom: {
+ extent: this._pathResults.bBoxNotchInsBot!.extent,
+ coords: this._pathResults.bBoxNotchInsBot!.coords,
+ },
+ };
+ }
+
+ public get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } } {
+ const results: { [id: string]: { extent: TExtent; coords: TCoords } } = {};
+
+ this._args.forEach(({ id }, index) => {
+ results[id] = {
+ extent: { width: 10, height: 10 }, // Example extent
+ coords: { x: 0, y: index * 20 }, // Example coordinates calculation
+ };
+ });
+
+ return results;
+ }
+
+ public get renderProps(): TBrickRenderPropsStatement {
+ return {
+ path: this._pathResults.path,
+ label: this._label,
+ labelArgs: this._args.map(({ label }) => label),
+ boundingBoxArgs: this._args.map(({ id }) => this._boundingBoxArgs[id]),
+ glyph: this._glyph,
+ colorBg: !this._highlighted ? this._colorBg : this._colorBgHighlight,
+ colorFg: !this._highlighted ? this._colorFg : this._colorFgHighlight,
+ outline: this._outline,
+ scale: this._scale,
+ };
+ }
+
+ public setBoundingBoxArg(id: string, extent: TExtent): void {
+ this._boundingBoxArgs[id] = extent;
+ }
+
+ public setConnectAbove(connectAbove: boolean): void {
+ this._connectAbove = connectAbove;
+ }
+
+ public setConnectBelow(connectBelow: boolean): void {
+ this._connectBelow = connectBelow;
+ }
+
+ public setHighlighted(highlighted: boolean): void {
+ this._highlighted = highlighted;
+ }
+}
diff --git a/modules/masonry/src/brick/design0/brickFactory.spec.ts b/modules/masonry/src/brick/design0/brickFactory.spec.ts
new file mode 100644
index 00000000..9774f996
--- /dev/null
+++ b/modules/masonry/src/brick/design0/brickFactory.spec.ts
@@ -0,0 +1,138 @@
+import {
+ createBrickBlock,
+ createBrickData,
+ createBrickExpression,
+ createBrickStatement,
+ getBrickFromWarehouse,
+ deleteBrickFromWarehouse,
+} from './brickFactory';
+import type { TColor } from '@/@types/brick';
+
+// Mock color data for testing
+const mockColor: TColor = '#FFFFFF';
+
+// Test suite for BrickFactory
+describe('BrickFactory', () => {
+ it('should create a BrickBlock and add it to the warehouse', () => {
+ const brick = createBrickBlock({
+ name: 'TestBlock',
+ label: 'BlockLabel',
+ glyph: '🔲',
+ args: [{ id: 'arg1', label: 'Arg 1' }],
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ connectAbove: true,
+ connectBelow: false,
+ nestLengthY: 100,
+ folded: false,
+ });
+
+ expect(brick).toBeDefined();
+ expect(getBrickFromWarehouse(brick.uuid)).toBe(brick);
+ });
+
+ it('should create a BrickData and add it to the warehouse', () => {
+ const brick = createBrickData({
+ name: 'TestData',
+ label: 'DataLabel',
+ glyph: '🔢',
+ dynamic: true,
+ value: 42,
+ input: 'number',
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ });
+
+ expect(brick).toBeDefined();
+ expect(getBrickFromWarehouse(brick.uuid)).toBe(brick);
+ });
+
+ it('should create a BrickExpression and add it to the warehouse', () => {
+ const brick = createBrickExpression({
+ name: 'TestExpression',
+ label: 'ExpressionLabel',
+ glyph: '📐',
+ args: { arg1: { label: 'Arg 1', dataType: 'number', meta: {} } },
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ });
+
+ expect(brick).toBeDefined();
+ expect(getBrickFromWarehouse(brick.uuid)).toBe(brick);
+ });
+
+ it('should create a BrickStatement and add it to the warehouse', () => {
+ const brick = createBrickStatement({
+ name: 'TestStatement',
+ label: 'StatementLabel',
+ glyph: '📄',
+ args: { arg1: { label: 'Arg 1', dataType: 'string', meta: {} } },
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ connectAbove: true,
+ connectBelow: true,
+ });
+
+ expect(brick).toBeDefined();
+ expect(getBrickFromWarehouse(brick.uuid)).toBe(brick);
+ });
+
+ it('should retrieve a brick from the warehouse by its UUID', () => {
+ const brick = createBrickBlock({
+ name: 'RetrieveBlock',
+ label: 'RetrieveLabel',
+ glyph: '🔲',
+ args: [{ id: 'arg2', label: 'Arg 2' }],
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ connectAbove: true,
+ connectBelow: true,
+ nestLengthY: 200,
+ });
+
+ const retrievedBrick = getBrickFromWarehouse(brick.uuid);
+ expect(retrievedBrick).toBe(brick);
+ });
+
+ it('should delete a brick from the warehouse by its UUID', () => {
+ const brick = createBrickBlock({
+ name: 'DeleteBlock',
+ label: 'DeleteLabel',
+ glyph: '🔲',
+ args: [{ id: 'arg3', label: 'Arg 3' }],
+ colorBg: mockColor,
+ colorFg: mockColor,
+ colorBgHighlight: mockColor,
+ colorFgHighlight: mockColor,
+ outline: mockColor,
+ scale: 1,
+ connectAbove: false,
+ connectBelow: true,
+ nestLengthY: 50,
+ });
+
+ const wasDeleted = deleteBrickFromWarehouse(brick.uuid);
+ expect(wasDeleted).toBe(true);
+ expect(getBrickFromWarehouse(brick.uuid)).toBeUndefined();
+ });
+});
diff --git a/modules/masonry/src/brick/design0/brickFactory.ts b/modules/masonry/src/brick/design0/brickFactory.ts
new file mode 100644
index 00000000..3dff1270
--- /dev/null
+++ b/modules/masonry/src/brick/design0/brickFactory.ts
@@ -0,0 +1,164 @@
+import { v4 as uuidv4 } from 'uuid';
+import type { TColor } from '@/@types/brick';
+import BrickStatement from './BrickStatement';
+import BrickExpression from './BrickExpression';
+import BrickData from './BrickData';
+import BrickBlock from './BrickBlock';
+
+// Warehouse to manage brick instances
+const brickWarehouse: Map =
+ new Map();
+
+/**
+ * Adds a brick instance to the warehouse.
+ * @param brick - The brick instance to add.
+ */
+function addBrickToWarehouse(
+ brick: BrickStatement | BrickExpression | BrickData | BrickBlock,
+): void {
+ brickWarehouse.set(brick.uuid, brick);
+}
+
+/**
+ * Retrieves a brick instance from the warehouse by its ID.
+ * @param id - The ID of the brick to retrieve.
+ * @returns The brick instance if found, otherwise undefined.
+ */
+function getBrickFromWarehouse(
+ id: string,
+): BrickStatement | BrickExpression | BrickData | BrickBlock | undefined {
+ return brickWarehouse.get(id);
+}
+
+/**
+ * Deletes a brick instance from the warehouse by its ID.
+ * @param id - The ID of the brick to delete.
+ * @returns True if the brick was deleted, false if it was not found.
+ */
+function deleteBrickFromWarehouse(id: string): boolean {
+ return brickWarehouse.delete(id);
+}
+
+/**
+ * Converts a Record type args to an array.
+ * @param argsRecord - Record of args objects.
+ * @returns Converted args as an array.
+ */
+function argsRecordToArray(
+ argsRecord: Record,
+): { id: string; label: string }[] {
+ return Object.entries(argsRecord).map(([id, { label }]) => ({ id, label }));
+}
+
+/**
+ * Factory function to create a new BrickBlock instance.
+ * @param params - Parameters to initialize the BrickBlock.
+ * @returns A new instance of BrickBlock.
+ */
+export function createBrickBlock(params: {
+ name: string;
+ label: string;
+ glyph?: string;
+ args: { id: string; label: string }[];
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ scale: number;
+ connectAbove: boolean;
+ connectBelow: boolean;
+ nestLengthY: number;
+ folded?: boolean;
+}): BrickBlock {
+ const brick = new BrickBlock({
+ uuid: uuidv4(),
+ ...params,
+ });
+ addBrickToWarehouse(brick);
+ return brick;
+}
+
+/**
+ * Factory function to create a new BrickData instance.
+ * @param params - Parameters to initialize the BrickData.
+ * @returns A new instance of BrickData.
+ */
+export function createBrickData(params: {
+ name: string;
+ label: string;
+ glyph: string;
+ dynamic: boolean;
+ value?: boolean | number | string;
+ input?: 'boolean' | 'number' | 'string' | 'options';
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ scale: number;
+}): BrickData {
+ const brick = new BrickData({
+ uuid: uuidv4(),
+ ...params,
+ });
+ addBrickToWarehouse(brick);
+ return brick;
+}
+
+/**
+ * Factory function to create a new BrickExpression instance.
+ * @param params - Parameters to initialize the BrickExpression.
+ * @returns A new instance of BrickExpression.
+ */
+export function createBrickExpression(params: {
+ name: string;
+ label: string;
+ glyph: string;
+ args: Record;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ scale: number;
+}): BrickExpression {
+ const brick = new BrickExpression({
+ uuid: uuidv4(),
+ ...params,
+ args: argsRecordToArray(params.args), // Convert Record to array
+ });
+ addBrickToWarehouse(brick);
+ return brick;
+}
+
+/**
+ * Factory function to create a new BrickStatement instance.
+ * @param params - Parameters to initialize the BrickStatement.
+ * @returns A new instance of BrickStatement.
+ */
+export function createBrickStatement(params: {
+ name: string;
+ label: string;
+ glyph: string;
+ args: Record;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ scale: number;
+ connectAbove: boolean;
+ connectBelow: boolean;
+}): BrickStatement {
+ const brick = new BrickStatement({
+ uuid: uuidv4(),
+ ...params,
+ args: argsRecordToArray(params.args),
+ });
+ addBrickToWarehouse(brick);
+ return brick;
+}
+
+// Exporting warehouse functions for external use
+export { addBrickToWarehouse, getBrickFromWarehouse, deleteBrickFromWarehouse };
diff --git a/modules/masonry/src/brick/design0/components/BrickBlock.tsx b/modules/masonry/src/brick/design0/components/BrickBlock.tsx
new file mode 100644
index 00000000..7be85bd6
--- /dev/null
+++ b/modules/masonry/src/brick/design0/components/BrickBlock.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import type { TBrickRenderPropsBlock } from '@/@types/brick';
+
+const BrickBlock: React.FC = ({
+ path,
+ label,
+ labelArgs,
+ colorBg,
+ colorFg,
+ outline,
+ scale,
+}) => {
+ return (
+
+
+
+ {label}
+
+ {labelArgs.map((argLabel, index) => (
+
+ {argLabel}
+
+ ))}
+ {/* {glyph && (
+
+ {instance.glyph}
+
+ )} */}
+
+ );
+};
+
+export default BrickBlock;
diff --git a/modules/masonry/src/brick/design0/components/BrickData.tsx b/modules/masonry/src/brick/design0/components/BrickData.tsx
new file mode 100644
index 00000000..05da3db0
--- /dev/null
+++ b/modules/masonry/src/brick/design0/components/BrickData.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import type { TBrickRenderPropsData, TCoords } from '@/@types/brick';
+
+interface BrickDataProps {
+ instance: TBrickRenderPropsData;
+ coords?: TCoords;
+}
+
+const BrickData: React.FC = ({ instance, coords = { x: 0, y: 0 } }) => {
+ return (
+
+
+
+ {instance.label}
+
+ {instance.glyph && (
+
+ {instance.glyph}
+
+ )}
+
+ );
+};
+
+export default BrickData;
diff --git a/modules/masonry/src/brick/design0/components/BrickExpression.tsx b/modules/masonry/src/brick/design0/components/BrickExpression.tsx
new file mode 100644
index 00000000..6fd659ae
--- /dev/null
+++ b/modules/masonry/src/brick/design0/components/BrickExpression.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import type { TBrickRenderPropsExpression, TCoords } from '@/@types/brick';
+
+interface BrickExpressionProps {
+ instance: TBrickRenderPropsExpression;
+ coords?: TCoords;
+}
+
+const BrickExpression: React.FC = ({ instance, coords = { x: 0, y: 0 } }) => {
+ return (
+
+
+
+ {instance.label}
+
+ {instance.labelArgs.map((argLabel, index) => (
+
+ {argLabel}
+
+ ))}
+ {instance.glyph && (
+
+ {instance.glyph}
+
+ )}
+
+ );
+};
+
+export default BrickExpression;
diff --git a/modules/masonry/src/brick/design0/components/BrickStatement.tsx b/modules/masonry/src/brick/design0/components/BrickStatement.tsx
new file mode 100644
index 00000000..f1595297
--- /dev/null
+++ b/modules/masonry/src/brick/design0/components/BrickStatement.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import type { TBrickRenderPropsStatement, TCoords } from '@/@types/brick';
+
+interface BrickStatementProps {
+ instance: TBrickRenderPropsStatement;
+ coords?: TCoords;
+}
+
+const BrickStatement: React.FC = ({ instance, coords = { x: 0, y: 0 } }) => {
+ return (
+
+
+
+ {instance.label}
+
+ {instance.labelArgs.map((argLabel, index) => (
+
+ {argLabel}
+
+ ))}
+ {instance.glyph && (
+
+ {instance.glyph}
+
+ )}
+
+ );
+};
+
+export default BrickStatement;
diff --git a/modules/masonry/src/brick/design0/components/BrickWrapper.tsx b/modules/masonry/src/brick/design0/components/BrickWrapper.tsx
new file mode 100644
index 00000000..3a1a0425
--- /dev/null
+++ b/modules/masonry/src/brick/design0/components/BrickWrapper.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import {
+ createBrickBlock,
+ createBrickData,
+ createBrickExpression,
+ createBrickStatement,
+} from '../brickFactory';
+import BrickBlockComponent from './BrickBlock';
+import BrickDataComponent from './BrickData';
+import BrickExpressionComponent from './BrickExpression';
+import BrickStatementComponent from './BrickStatement';
+import type {
+ TBrickRenderPropsBlock,
+ TBrickRenderPropsData,
+ TBrickRenderPropsExpression,
+ TBrickRenderPropsStatement,
+ TCoords,
+} from '@/@types/brick';
+
+type TBrickWrapperProps =
+ | { type: 'block'; params: TBrickRenderPropsBlock; coords?: TCoords }
+ | { type: 'data'; params: TBrickRenderPropsData; coords?: TCoords }
+ | { type: 'expression'; params: TBrickRenderPropsExpression; coords?: TCoords }
+ | { type: 'statement'; params: TBrickRenderPropsStatement; coords?: TCoords };
+
+const BrickWrapper: React.FC = ({ type, params, coords }) => {
+ switch (type) {
+ case 'block': {
+ const instance = createBrickBlock({
+ name: params.label,
+ label: params.label,
+ glyph: params.glyph || '',
+ args: params.labelArgs.map((label, index) => ({ id: `arg${index}`, label })),
+ colorBg: params.colorBg,
+ colorFg: params.colorFg,
+ colorBgHighlight: params.colorBg,
+ colorFgHighlight: params.colorFg,
+ outline: params.outline,
+ scale: params.scale,
+ connectAbove: true,
+ connectBelow: true,
+ nestLengthY: 0,
+ folded: params.folded,
+ });
+ return ;
+ }
+ case 'data': {
+ const instance = createBrickData({
+ name: params.label,
+ label: params.label,
+ glyph: params.glyph || '',
+ dynamic: true,
+ colorBg: params.colorBg,
+ colorFg: params.colorFg,
+ colorBgHighlight: params.colorBg,
+ colorFgHighlight: params.colorFg,
+ outline: params.outline,
+ scale: params.scale,
+ });
+ return ;
+ }
+ case 'expression': {
+ const instance = createBrickExpression({
+ name: params.label,
+ label: params.label,
+ glyph: params.glyph || '',
+ args: params.labelArgs.reduce((acc, label, index) => {
+ acc[`arg${index}`] = { label, dataType: 'unknown', meta: {} };
+ return acc;
+ }, {} as Record),
+ colorBg: params.colorBg,
+ colorFg: params.colorFg,
+ colorBgHighlight: params.colorBg,
+ colorFgHighlight: params.colorFg,
+ outline: params.outline,
+ scale: params.scale,
+ });
+ return ;
+ }
+ case 'statement': {
+ const instance = createBrickStatement({
+ name: params.label,
+ label: params.label,
+ glyph: params.glyph || '',
+ args: params.labelArgs.reduce((acc, label, index) => {
+ acc[`arg${index}`] = { label, dataType: 'unknown', meta: {} };
+ return acc;
+ }, {} as Record),
+ colorBg: params.colorBg,
+ colorFg: params.colorFg,
+ colorBgHighlight: params.colorBg,
+ colorFgHighlight: params.colorFg,
+ outline: params.outline,
+ scale: params.scale,
+ connectAbove: true,
+ connectBelow: true,
+ });
+ return ;
+ }
+ default:
+ return null;
+ }
+};
+
+export default BrickWrapper;
diff --git a/modules/masonry/src/brick/design0/stories/BrickBlock.stories.ts b/modules/masonry/src/brick/design0/stories/BrickBlock.stories.ts
new file mode 100644
index 00000000..ef30eb05
--- /dev/null
+++ b/modules/masonry/src/brick/design0/stories/BrickBlock.stories.ts
@@ -0,0 +1,40 @@
+import { MetaData, Story } from '../../stories/brickBlock';
+import MBrickBlock from '../BrickBlock';
+import CBrickBlock from '../components/BrickBlock';
+
+export default {
+ title: 'Design 0/Block Brick',
+ ...MetaData,
+};
+
+// -------------------------------------------------------------------------------------------------
+
+export const NoArgs: Story = {
+ args: {
+ View: CBrickBlock,
+ Model: MBrickBlock,
+ showIndicators: true,
+
+ label: 'Block',
+ args: [],
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
+
+export const WithArgs: Story = {
+ args: {
+ View: CBrickBlock,
+ Model: MBrickBlock,
+ showIndicators: true,
+
+ label: 'Block',
+ args: ['Label 1', 'Label 2'],
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
diff --git a/modules/masonry/src/brick/design0/stories/BrickData.stories.ts b/modules/masonry/src/brick/design0/stories/BrickData.stories.ts
new file mode 100644
index 00000000..962108c8
--- /dev/null
+++ b/modules/masonry/src/brick/design0/stories/BrickData.stories.ts
@@ -0,0 +1,22 @@
+import { MetaData, Story } from '../../stories/brickData';
+import MBrickData from '../BrickData';
+import CBrickData from '../components/BrickData';
+
+export default {
+ title: 'Design 0/Data Brick',
+ ...MetaData,
+};
+
+// -------------------------------------------------------------------------------------------------
+
+export const Static: Story = {
+ args: {
+ Component: CBrickData,
+ prototype: MBrickData,
+ label: 'Data',
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
diff --git a/modules/masonry/src/brick/design0/stories/BrickExpression.stories.ts b/modules/masonry/src/brick/design0/stories/BrickExpression.stories.ts
new file mode 100644
index 00000000..b47a3faa
--- /dev/null
+++ b/modules/masonry/src/brick/design0/stories/BrickExpression.stories.ts
@@ -0,0 +1,23 @@
+import { MetaData, Story } from '../../stories/brickExpression';
+import MBrickExpression from '../BrickExpression';
+import CBrickExpression from '../components/BrickExpression';
+
+export default {
+ title: 'Design 0/Expression Brick',
+ ...MetaData,
+};
+
+// -------------------------------------------------------------------------------------------------
+
+export const WithArgs: Story = {
+ args: {
+ Component: CBrickExpression,
+ prototype: MBrickExpression,
+ label: 'Expression',
+ args: ['Label 1'],
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
diff --git a/modules/masonry/src/brick/design0/stories/BrickStatement.stories.ts b/modules/masonry/src/brick/design0/stories/BrickStatement.stories.ts
new file mode 100644
index 00000000..404f2763
--- /dev/null
+++ b/modules/masonry/src/brick/design0/stories/BrickStatement.stories.ts
@@ -0,0 +1,36 @@
+import { MetaData, Story } from '../../stories/brickStatement';
+import MBrickStatement from '../BrickStatement';
+import CBrickStatement from '../components/BrickStatement';
+
+export default {
+ title: 'Design 0/Statement Brick',
+ ...MetaData,
+};
+
+// -------------------------------------------------------------------------------------------------
+
+export const NoArgs: Story = {
+ args: {
+ Component: CBrickStatement,
+ prototype: MBrickStatement,
+ label: 'Statement',
+ args: [],
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
+
+export const WithArgs: Story = {
+ args: {
+ Component: CBrickStatement,
+ prototype: MBrickStatement,
+ label: 'Statement',
+ args: ['Label 1'],
+ colorBg: 'yellow',
+ colorFg: 'black',
+ outline: 'red',
+ scale: 1,
+ },
+};
diff --git a/modules/masonry/src/brick/index.ts b/modules/masonry/src/brick/index.ts
new file mode 100644
index 00000000..6718a5cd
--- /dev/null
+++ b/modules/masonry/src/brick/index.ts
@@ -0,0 +1,9 @@
+export { default as ModelBrickData } from './design0/BrickData';
+export { default as ModelBrickExpression } from './design0/BrickExpression';
+export { default as ModelBrickStatement } from './design0/BrickStatement';
+export { default as ModelBrickBlock } from './design0/BrickBlock';
+
+export { default as BrickData } from './design0/components/BrickData';
+export { default as BrickExpression } from './design0/components/BrickExpression';
+export { default as BrickStatement } from './design0/components/BrickStatement';
+export { default as BrickBlock } from './design0/components/BrickBlock';
diff --git a/modules/masonry/src/brick/model.ts b/modules/masonry/src/brick/model.ts
new file mode 100644
index 00000000..dbbbceb5
--- /dev/null
+++ b/modules/masonry/src/brick/model.ts
@@ -0,0 +1,360 @@
+import type {
+ IBrick,
+ IBrickArgument,
+ IBrickBlock,
+ IBrickData,
+ IBrickExpression,
+ IBrickInstruction,
+ IBrickStatement,
+ TBrickRenderPropsData,
+ TBrickRenderPropsExpression,
+ TBrickRenderPropsStatement,
+ TBrickRenderPropsBlock,
+ TBrickKind,
+ TBrickType,
+ TColor,
+ TCoords,
+ TExtent,
+} from '@/@types/brick';
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a generic brick.
+ */
+abstract class BrickModel implements IBrick {
+ protected _uuid: string;
+ protected _name: string;
+ protected _kind: TBrickKind;
+ protected _type: TBrickType;
+
+ protected _label: string;
+ protected _glyph: string;
+ protected _colorBg: TColor;
+ protected _colorFg: TColor;
+ protected _colorBgHighlight: TColor;
+ protected _colorFgHighlight: TColor;
+ protected _outline: TColor;
+
+ protected _highlighted = false;
+ protected _scale = 1;
+
+ constructor(params: {
+ /** unique ID */
+ uuid: string;
+ /** name — to be used for internal bookkeeping */
+ name: string;
+ /** kind — instruction or argument */
+ kind: TBrickKind;
+ /** type — data, expression, statement, or block */
+ type: TBrickType;
+ /** primary label */
+ label: string;
+ /** glyph icon associated with the brick */
+ glyph?: string;
+ /** primary background color */
+ colorBg: TColor;
+ /** primary foreground color */
+ colorFg: TColor;
+ /** highlighted state background color */
+ colorBgHighlight: TColor;
+ /** highlighted state foreground color */
+ colorFgHighlight: TColor;
+ /** outline/stroke color */
+ outline: TColor;
+ }) {
+ this._uuid = params.uuid;
+ this._name = params.name;
+ this._kind = params.kind;
+ this._type = params.type;
+
+ this._label = params.label;
+ this._glyph = params.glyph ?? '';
+ this._colorBg = params.colorBg;
+ this._colorFg = params.colorFg;
+ this._colorBgHighlight = params.colorBgHighlight;
+ this._colorFgHighlight = params.colorFgHighlight;
+ this._outline = params.outline;
+ }
+
+ public get uuid(): string {
+ return this._uuid;
+ }
+
+ public get name(): string {
+ return this._name;
+ }
+
+ public get kind(): TBrickKind {
+ return this._kind;
+ }
+
+ public get type(): TBrickType {
+ return this._type;
+ }
+
+ public set highlighted(value: boolean) {
+ this._highlighted = value;
+ }
+
+ public set scale(value: number) {
+ this._scale = value;
+ }
+
+ public abstract get boundingBox(): TExtent;
+
+ public abstract get connPointsFixed(): Record;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a generic argument brick.
+ */
+abstract class BrickModelArgument extends BrickModel implements IBrickArgument {
+ constructor(params: {
+ uuid: string;
+ name: string;
+ type: TBrickType;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ }) {
+ super({ ...params, kind: 'argument' });
+ }
+
+ public abstract get connPointsFixed(): Record<
+ 'argOutgoing',
+ { extent: TExtent; coords: TCoords }
+ >;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a generic instruction brick.
+ */
+abstract class BrickModelInstruction extends BrickModel implements IBrickInstruction {
+ protected _connectAbove: boolean;
+ protected _connectBelow: boolean;
+
+ protected _args: { id: string; label: string }[] = [];
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+ type: TBrickType;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ connectAbove: boolean;
+ connectBelow: boolean;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) {
+ super({ ...params, kind: 'instruction' });
+
+ this._connectAbove = params.connectAbove;
+ this._connectBelow = params.connectBelow;
+
+ this._args = params.args;
+ }
+
+ public get connectAbove(): boolean {
+ return this._connectAbove;
+ }
+
+ public get connectBelow(): boolean {
+ return this._connectBelow;
+ }
+
+ public abstract get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } };
+
+ public abstract setBoundingBoxArg(id: string, extent: TExtent): void;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a data brick.
+ */
+export abstract class BrickModelData extends BrickModelArgument implements IBrickData {
+ protected _dynamic: boolean;
+ protected _value?: boolean | number | string;
+ protected _input?: 'boolean' | 'number' | 'string' | 'options';
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+
+ dynamic: boolean;
+ value?: boolean | number | string;
+ input?: 'boolean' | 'number' | 'string' | 'options';
+ }) {
+ super({ ...params, type: 'data' });
+
+ this._dynamic = params.dynamic;
+ this._value = params.value;
+ this._input = params.input;
+ }
+
+ public get dynamic(): boolean {
+ return this._dynamic;
+ }
+
+ public get value(): boolean | number | string | undefined {
+ return this._value;
+ }
+
+ public get input(): 'boolean' | 'number' | 'string' | 'options' | undefined {
+ return this._input;
+ }
+
+ public abstract get renderProps(): TBrickRenderPropsData;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of an expression brick.
+ */
+export abstract class BrickModelExpression extends BrickModelArgument implements IBrickExpression {
+ protected _args: { id: string; label: string }[] = [];
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) {
+ super({ ...params, type: 'expression' });
+
+ this._args = params.args;
+ }
+
+ public abstract get connPointsArg(): { [id: string]: { extent: TExtent; coords: TCoords } };
+
+ public abstract setBoundingBoxArg(id: string, extent: TExtent): void;
+
+ public abstract get renderProps(): TBrickRenderPropsExpression;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a statement brick.
+ */
+export abstract class BrickModelStatement extends BrickModelInstruction implements IBrickStatement {
+ constructor(params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ connectAbove: boolean;
+ connectBelow: boolean;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) {
+ super({ ...params, type: 'statement' });
+ }
+
+ public abstract get connPointsFixed(): Record<
+ 'insTop' | 'insBottom',
+ { extent: TExtent; coords: TCoords }
+ >;
+
+ public abstract get renderProps(): TBrickRenderPropsStatement;
+}
+
+/**
+ * @abstract
+ * @class
+ * Defines the data model of a block brick.
+ */
+export abstract class BrickModelBlock extends BrickModelInstruction implements IBrickBlock {
+ protected _folded = false;
+
+ constructor(params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ connectAbove: boolean;
+ connectBelow: boolean;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) {
+ super({ ...params, type: 'block' });
+ }
+
+ public set folded(value: boolean) {
+ this._folded = value;
+ }
+
+ public abstract get connPointsFixed(): Record<
+ 'insTop' | 'insBottom' | 'insNest',
+ { extent: TExtent; coords: TCoords }
+ >;
+
+ public abstract get renderProps(): TBrickRenderPropsBlock;
+
+ public abstract setBoundingBoxNest(extent: TExtent): void;
+}
diff --git a/modules/masonry/src/brick/stories/brickBlock.ts b/modules/masonry/src/brick/stories/brickBlock.ts
new file mode 100644
index 00000000..154b50e7
--- /dev/null
+++ b/modules/masonry/src/brick/stories/brickBlock.ts
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import CBrickBlock from './components/BrickBlock';
+
+// -------------------------------------------------------------------------------------------------
+
+export const MetaData: Meta = {
+ component: CBrickBlock,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export type Story = StoryObj;
diff --git a/modules/masonry/src/brick/stories/brickData.ts b/modules/masonry/src/brick/stories/brickData.ts
new file mode 100644
index 00000000..3c9268d5
--- /dev/null
+++ b/modules/masonry/src/brick/stories/brickData.ts
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import CBrickData from './components/BrickData';
+
+// -------------------------------------------------------------------------------------------------
+
+export const MetaData: Meta = {
+ component: CBrickData,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export type Story = StoryObj;
diff --git a/modules/masonry/src/brick/stories/brickExpression.ts b/modules/masonry/src/brick/stories/brickExpression.ts
new file mode 100644
index 00000000..02b89299
--- /dev/null
+++ b/modules/masonry/src/brick/stories/brickExpression.ts
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import CBrickExpression from './components/BrickExpression';
+
+// -------------------------------------------------------------------------------------------------
+
+export const MetaData: Meta = {
+ component: CBrickExpression,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export type Story = StoryObj;
diff --git a/modules/masonry/src/brick/stories/brickStatement.ts b/modules/masonry/src/brick/stories/brickStatement.ts
new file mode 100644
index 00000000..0dd82a9f
--- /dev/null
+++ b/modules/masonry/src/brick/stories/brickStatement.ts
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import CBrickStatement from './components/BrickStatement';
+
+// -------------------------------------------------------------------------------------------------
+
+export const MetaData: Meta = {
+ component: CBrickStatement,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export type Story = StoryObj;
diff --git a/modules/masonry/src/brick/stories/components/BrickBlock.tsx b/modules/masonry/src/brick/stories/components/BrickBlock.tsx
new file mode 100644
index 00000000..c65748b1
--- /dev/null
+++ b/modules/masonry/src/brick/stories/components/BrickBlock.tsx
@@ -0,0 +1,122 @@
+import type { JSX } from 'react';
+import type { IBrickBlock, TBrickRenderPropsBlock, TColor } from '@/@types/brick';
+
+import BrickWrapper from './BrickWrapper';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function (props: {
+ View: React.FC;
+ Model: new (params: {
+ uuid: string;
+ name: string;
+
+ label: string;
+ glyph?: string;
+ colorBg: TColor;
+ colorFg: TColor;
+ colorBgHighlight: TColor;
+ colorFgHighlight: TColor;
+ outline: TColor;
+ connectAbove: boolean;
+ connectBelow: boolean;
+
+ args: {
+ /** unique identifier of the argument */
+ id: string;
+ /** label for the argument */
+ label: string;
+ }[];
+ }) => IBrickBlock;
+ showIndicators: boolean;
+
+ label: string;
+ args: string[];
+ colorBg: string;
+ colorFg: string;
+ outline: string;
+ scale: number;
+}): JSX.Element {
+ const { View, Model, showIndicators, label, args, colorBg, colorFg, outline, scale } = props;
+
+ const instance = new Model({
+ uuid: '',
+ name: '',
+
+ label,
+ colorBg,
+ colorFg,
+ colorBgHighlight: '',
+ colorFgHighlight: '',
+ outline,
+ connectAbove: true,
+ connectBelow: true,
+
+ args: args.map((label, i) => ({ id: `label_${i}`, label })),
+ });
+
+ instance.scale = scale;
+
+ const VisualIndicators = () => (
+ <>
+ {/* Overall Bounding Box of the Brick */}
+
+
+ {/* Connection point of Top */}
+
+
+ {/* Connection point of Bottom */}
+
+
+ {/* Connection point of Nesting */}
+
+
+ {/* Connection points of Args */}
+ {Object.values(instance.connPointsArg).map(({ extent, coords }) => (
+
+ ))}
+ >
+ );
+
+ return (
+
+
+ {showIndicators && }
+
+ );
+}
diff --git a/modules/masonry/src/brick/stories/components/BrickData.tsx b/modules/masonry/src/brick/stories/components/BrickData.tsx
new file mode 100644
index 00000000..217aa774
--- /dev/null
+++ b/modules/masonry/src/brick/stories/components/BrickData.tsx
@@ -0,0 +1,72 @@
+import BrickWrapper from './BrickWrapper';
+import type { JSX } from 'react';
+import type { IBrickData, TBrickArgDataType, TBrickColor } from '@/@types/brick';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function (props: {
+ Component: (props: { instance: IBrickData; visualIndicators?: JSX.Element }) => JSX.Element;
+ prototype: new (params: {
+ name: string;
+ label: string;
+ glyph: string;
+ dataType: TBrickArgDataType;
+ dynamic: boolean;
+ value?: boolean | number | string;
+ input?: 'boolean' | 'number' | 'string' | 'options';
+ colorBg: TBrickColor;
+ colorFg: TBrickColor;
+ outline: TBrickColor;
+ scale: number;
+ }) => IBrickData;
+ label: string;
+ colorBg: string;
+ colorFg: string;
+ outline: string;
+ scale: number;
+}): JSX.Element {
+ const { Component, prototype, label, colorBg, colorFg, outline, scale } = props;
+
+ const instance = new prototype({
+ label,
+ colorBg,
+ colorFg,
+ outline,
+ scale,
+ glyph: '',
+ dynamic: false,
+ dataType: 'any',
+ name: '',
+ });
+
+ const VisualIndicators = () => (
+ <>
+ {/* Overall Bounding Box of the Brick */}
+
+
+ {/* Left notch bounding box */}
+
+ >
+ );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/modules/masonry/src/brick/stories/components/BrickExpression.tsx b/modules/masonry/src/brick/stories/components/BrickExpression.tsx
new file mode 100644
index 00000000..8d1a1b69
--- /dev/null
+++ b/modules/masonry/src/brick/stories/components/BrickExpression.tsx
@@ -0,0 +1,100 @@
+import BrickWrapper from './BrickWrapper';
+import type { JSX } from 'react';
+import type { IBrickExpression, TBrickArgDataType, TBrickColor } from '@/@types/brick';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function (props: {
+ Component: (props: { instance: IBrickExpression; visualIndicators?: JSX.Element }) => JSX.Element;
+ prototype: new (params: {
+ name: string;
+ label: string;
+ glyph: string;
+ dataType: TBrickArgDataType;
+ args: Record<
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ }
+ >;
+ colorBg: TBrickColor;
+ colorFg: TBrickColor;
+ outline: TBrickColor;
+ scale: number;
+ }) => IBrickExpression;
+ label: string;
+ args: string[];
+ colorBg: string;
+ colorFg: string;
+ outline: string;
+ scale: number;
+}): JSX.Element {
+ const { Component, prototype, label, args, colorBg, colorFg, outline, scale } = props;
+
+ const instance = new prototype({
+ label,
+ args: Object.fromEntries(
+ args.map<[string, { label: string; dataType: TBrickArgDataType; meta: unknown }]>((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg,
+ colorFg,
+ outline,
+ scale,
+ glyph: '',
+ dataType: 'any',
+ name: '',
+ });
+
+ const VisualIndicators = () => (
+ <>
+ {/* Overall Bounding Box of the Brick */}
+
+
+ {/* Right args bounding box */}
+ {Object.keys(instance.bBoxArgs).map((name, i) => {
+ const arg = instance.bBoxArgs[name];
+
+ return (
+
+ );
+ })}
+
+ {/* Left notch bounding box */}
+
+ >
+ );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/modules/masonry/src/brick/stories/components/BrickStatement.tsx b/modules/masonry/src/brick/stories/components/BrickStatement.tsx
new file mode 100644
index 00000000..b68c3969
--- /dev/null
+++ b/modules/masonry/src/brick/stories/components/BrickStatement.tsx
@@ -0,0 +1,112 @@
+import BrickWrapper from './BrickWrapper';
+import type { JSX } from 'react';
+import type { IBrickStatement, TBrickArgDataType, TBrickColor } from '@/@types/brick';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function (props: {
+ Component: (props: { instance: IBrickStatement; visualIndicators?: JSX.Element }) => JSX.Element;
+ prototype: new (params: {
+ name: string;
+ label: string;
+ glyph: string;
+ args: Record<
+ string,
+ {
+ label: string;
+ dataType: TBrickArgDataType;
+ meta: unknown;
+ }
+ >;
+ colorBg: TBrickColor;
+ colorFg: TBrickColor;
+ outline: TBrickColor;
+ scale: number;
+ connectAbove: boolean;
+ connectBelow: boolean;
+ }) => IBrickStatement;
+ label: string;
+ args: string[];
+ colorBg: string;
+ colorFg: string;
+ outline: string;
+ scale: number;
+}): JSX.Element {
+ const { Component, prototype, label, args, colorBg, colorFg, outline, scale } = props;
+
+ const instance = new prototype({
+ label,
+ args: Object.fromEntries(
+ args.map<[string, { label: string; dataType: TBrickArgDataType; meta: unknown }]>((name) => [
+ name,
+ { label: name, dataType: 'any', meta: undefined },
+ ]),
+ ),
+ colorBg,
+ colorFg,
+ outline,
+ scale,
+ glyph: '',
+ connectAbove: true,
+ connectBelow: true,
+ name: '',
+ });
+
+ const VisualIndicators = () => (
+ <>
+ {/* Overall Bounding Box of the Brick */}
+
+
+ {/* Right args bounding box */}
+ {Object.keys(instance.bBoxArgs).map((name, i) => {
+ const arg = instance.bBoxArgs[name];
+
+ return (
+
+ );
+ })}
+
+ {/* Top instruction notch bounding box */}
+
+
+ {/* Bottom instruction notch bounding box */}
+
+ >
+ );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/modules/masonry/src/brick/stories/components/BrickWrapper.tsx b/modules/masonry/src/brick/stories/components/BrickWrapper.tsx
new file mode 100644
index 00000000..74fa20ec
--- /dev/null
+++ b/modules/masonry/src/brick/stories/components/BrickWrapper.tsx
@@ -0,0 +1,11 @@
+import type { PropsWithChildren } from 'react';
+
+// -------------------------------------------------------------------------------------------------
+
+export default function (props: PropsWithChildren): JSX.Element {
+ return (
+
+ );
+}
diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts
new file mode 100644
index 00000000..f8b3b276
--- /dev/null
+++ b/modules/masonry/src/brick/utils/path.ts
@@ -0,0 +1,605 @@
+// == constants ====================================================================================
+
+const cornerRadius = 4;
+const strokeWidth = 0.5;
+
+const notchInsOffsetX = 4;
+const notchInsLengthX = 10;
+const notchInsLengthY = 2;
+
+const notchArgLengthX = 8;
+const notchArgLengthY = 12;
+const notchArgBaseLengthX = 4;
+const notchArgBaseLengthY = 10;
+const notchArgStemLengthY = 4;
+
+const nestLengthYMin = cornerRadius * 2 + notchArgLengthY;
+const innerLengthXMin = cornerRadius * 2 + notchInsOffsetX * 2 + notchInsLengthX;
+
+// == private variables ============================================================================
+
+let _hasNest = false;
+let _hasNotchArg = false;
+let _hasNotchInsTop = false;
+let _hasNotchInsBot = false;
+
+let _scale = 1;
+let _nestLengthY = nestLengthYMin;
+let _innerLengthX = innerLengthXMin;
+let _argsLengthY: number[] = [];
+
+// == private functions ============================================================================
+
+/**
+ * Sets internal variables that control the shape of the path
+ * @param options options to control the shape of the path
+ *
+ * @private
+ */
+function _setOptions(options: {
+ hasNest: boolean;
+ hasNotchArg: boolean;
+ hasNotchInsTop: boolean;
+ hasNotchInsBot: boolean;
+ scale: number;
+ nestLengthY?: number;
+ innerLengthX: number;
+ argHeights: number[];
+}): void {
+ _hasNest = options.hasNest;
+
+ _hasNotchArg = options.hasNotchArg;
+ _hasNotchInsTop = options.hasNotchInsTop;
+ _hasNotchInsBot = options.hasNotchInsBot;
+
+ _scale = options.scale;
+ if (options.nestLengthY) _nestLengthY = Math.max(nestLengthYMin, options.nestLengthY);
+ _innerLengthX = Math.max(innerLengthXMin, options.innerLengthX);
+ _argsLengthY = options.argHeights;
+}
+
+/**
+ * Generates top section of the path (left arc to right arc)
+ *
+ * @remarks
+ * left to right
+ *
+ * @private
+ */
+function _getPathTop(): string[] {
+ const lineLengthX = _innerLengthX - cornerRadius * 2 - (notchInsOffsetX + notchInsLengthX);
+
+ return [
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 ${cornerRadius} -${cornerRadius}`,
+ `h ${notchInsOffsetX}`,
+ ...(_hasNotchInsTop
+ ? [
+ //
+ `v ${notchInsLengthY}`,
+ `h ${notchInsLengthX}`,
+ `v -${notchInsLengthY}`,
+ ]
+ : [
+ //
+ `h ${notchInsLengthX}`,
+ ]),
+ `h ${lineLengthX}`,
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 ${cornerRadius} ${cornerRadius}`,
+ ];
+}
+
+/**
+ * Generates bottom section of the path (right arc to left arc; includes nest)
+ *
+ * @remarks
+ * right to left
+ *
+ * @private
+ */
+function _getPathBottom(): string[] {
+ if (!_hasNest) {
+ const lineLengthX = _innerLengthX - cornerRadius * 2 - (notchInsOffsetX + notchInsLengthX);
+
+ return [
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 -${cornerRadius} ${cornerRadius}`,
+ `h -${lineLengthX}`,
+ ...(_hasNotchInsBot
+ ? [
+ 'h -1',
+ `v ${notchInsLengthY}`,
+ `h -${notchInsLengthX - 2}`,
+ `v -${notchInsLengthY}`,
+ 'h -1',
+ ]
+ : [
+ //
+ `h -${notchInsLengthX}`,
+ ]),
+ `h -${notchInsOffsetX}`,
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 -${cornerRadius} -${cornerRadius}`,
+ ];
+ }
+
+ const lineLengthX =
+ _innerLengthX -
+ cornerRadius * 2 -
+ (notchInsOffsetX + notchInsLengthX) -
+ (cornerRadius + notchInsOffsetX + 1);
+
+ return [
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 -${cornerRadius} ${cornerRadius}`,
+ `h -${lineLengthX}`,
+ 'h -1',
+ `v ${notchInsLengthY}`,
+ `h -${notchInsLengthX - 2}`,
+ `v -${notchInsLengthY}`,
+ 'h -1',
+ `h -${notchInsOffsetX}`,
+ `a ${cornerRadius + 1} ${cornerRadius + 1} 90 0 0 -${cornerRadius + 1} ${cornerRadius + 1}`,
+ `v ${_nestLengthY - cornerRadius * 2}`,
+ `a ${cornerRadius + 1} ${cornerRadius + 1} 90 0 0 ${cornerRadius + 1} ${cornerRadius + 1}`,
+ `h ${notchInsOffsetX}`,
+ `v ${notchInsLengthY}`,
+ `h ${notchInsLengthX}`,
+ `v -${notchInsLengthY}`,
+ `h ${notchInsOffsetX}`,
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 ${cornerRadius} ${cornerRadius}`,
+ `v ${notchArgLengthY}`,
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 -${cornerRadius} ${cornerRadius}`,
+ 'h -1',
+ `h -${cornerRadius + notchInsOffsetX * 2}`,
+ ...(_hasNotchInsBot
+ ? [
+ 'h -1',
+ `v ${notchInsLengthY}`,
+ `h -${notchInsLengthX - 2}`,
+ `v -${notchInsLengthY}`,
+ 'h -1',
+ ]
+ : [
+ //
+ `h -${notchInsLengthX}`,
+ ]),
+ `h -${notchInsOffsetX}`,
+ `a ${cornerRadius} ${cornerRadius} 90 0 1 -${cornerRadius} -${cornerRadius}`,
+ `v -${cornerRadius * 2 + notchArgLengthY}`,
+ 'v -1',
+ `v -${_nestLengthY - cornerRadius * 2}`,
+ 'v -1',
+ `v -${cornerRadius * 2}`,
+ ];
+}
+
+/**
+ * Generates the argument connector (concave) positioned on the right
+ *
+ * @remarks
+ * top to bottom
+ *
+ * @private
+ */
+function _getNotchArgInner(): string[] {
+ const endLineLengthY = (notchArgLengthY - notchArgStemLengthY) / 2;
+ const stemLengthX = notchArgLengthX - notchArgBaseLengthX;
+ const baseRiseLengthY = (notchArgBaseLengthY - notchArgStemLengthY) / 2;
+
+ return [
+ `v ${endLineLengthY}`,
+ `h -${stemLengthX}`,
+ `v -${baseRiseLengthY}`,
+ `h -${notchArgBaseLengthX}`,
+ `v ${notchArgBaseLengthY}`,
+ `h ${notchArgBaseLengthX}`,
+ `v -${baseRiseLengthY}`,
+ `h ${stemLengthX}`,
+ `v ${endLineLengthY}`,
+ ];
+}
+
+/**
+ * Generates the argument connector (convex) positioned on the left
+ *
+ * @remarks
+ * bottom to top
+ *
+ * @private
+ */
+function _getNotchArgOuter(): string[] {
+ const endLineLengthY = (notchArgLengthY - notchArgStemLengthY) / 2 + 1;
+ const stemLengthX = notchArgLengthX - notchArgBaseLengthX + 2;
+ const baseRiseLengthY = (notchArgBaseLengthY - notchArgStemLengthY) / 2;
+
+ return [
+ `v -${endLineLengthY}`,
+ `h -${stemLengthX}`,
+ `v ${baseRiseLengthY}`,
+ `h -${notchArgBaseLengthX - 2}`,
+ `v -${notchArgBaseLengthY - 2}`,
+ `h ${notchArgBaseLengthX - 2}`,
+ `v ${baseRiseLengthY}`,
+ `h ${stemLengthX}`,
+ `v -${endLineLengthY}`,
+ ];
+}
+
+/**
+ * Generates portion for one argument connector (concave) positioned on the right
+ * @param options determines shape
+ *
+ * @remarks
+ * top to bottom
+ *
+ * @private
+ */
+function _generateArgSection(options: {
+ /** whether protrude vertical top line */
+ hasOffsetTop: boolean;
+ /** whether protrude vertical bottom line */
+ hasOffsetBot: boolean;
+ /** total vertical length of the portion */
+ sectionLengthY: number;
+}): string[] {
+ const { hasOffsetTop, hasOffsetBot, sectionLengthY } = options;
+
+ const sectionOffsetTopY = hasOffsetTop ? cornerRadius : 0;
+ const sectionOffsetBotY = sectionLengthY - cornerRadius - notchArgLengthY - cornerRadius;
+
+ return [
+ //
+ `v ${sectionOffsetTopY}`,
+ ..._getNotchArgInner(),
+ `v ${hasOffsetBot ? cornerRadius : 0}`,
+ `v ${sectionOffsetBotY}`,
+ ];
+}
+
+/**
+ * Generates right section of the path (includes argument connectors)
+ *
+ * @remarks
+ * top to bottom
+ *
+ * @private
+ */
+function _getPathRight() {
+ const sectionLengthYMin = cornerRadius * 2 + notchArgLengthY;
+
+ return _argsLengthY.length === 0
+ ? [
+ //
+ `v ${notchArgLengthY}`,
+ ]
+ : _argsLengthY
+ .map((sectionLengthY, i) =>
+ _generateArgSection({
+ hasOffsetTop: i !== 0,
+ hasOffsetBot: i < _argsLengthY.length - 1,
+ sectionLengthY: Math.max(sectionLengthYMin, sectionLengthY),
+ }),
+ )
+ .reduce((a, b) => [...a, ...b], []);
+}
+
+/**
+ * Generates left section of the path (includes argument notch)
+ *
+ * @remarks
+ * bottom to top
+ *
+ * @private
+ */
+function _getPathLeft() {
+ const lineLengthY = Math.max(
+ 0,
+ _argsLengthY.reduce((a, b) => a + b, 0) - cornerRadius * 2 - notchArgLengthY,
+ );
+
+ return [
+ //
+ `v -${lineLengthY}`,
+ ...(_hasNotchArg
+ ? _getNotchArgOuter()
+ : [
+ //
+ `v -${notchArgLengthY}`,
+ ]),
+ ];
+}
+
+// -- private helper functions ---------------------------------------------------------------------
+
+/**
+ * Generates brick SVG path
+ *
+ * @private
+ */
+function _getPath(): string {
+ const offsetX = 0.5 + (_hasNotchArg ? notchArgLengthX : 0);
+ const offsetY = 0.5 + cornerRadius;
+
+ return [
+ `m ${offsetX} ${offsetY}`,
+ ...[_getPathTop(), _getPathRight(), _getPathBottom(), _getPathLeft()].map((sections) =>
+ sections.join(' '),
+ ),
+ 'z',
+ ].join(' ');
+}
+
+/**
+ * Generates bounding box of the brick (excludes notches)
+ *
+ * @private
+ */
+function _getBBoxBrick(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number };
+} {
+ const argSectionLengthYMin = cornerRadius * 2 + notchArgLengthY;
+ const argsLength = _argsLengthY
+ .map((sectionLengthY) => Math.max(argSectionLengthYMin, sectionLengthY))
+ .reduce((a, b) => a + b, 0);
+
+ let height =
+ strokeWidth +
+ (argsLength !== 0 ? argsLength : 2 * cornerRadius + notchArgLengthY) +
+ strokeWidth;
+
+ if (_hasNest) {
+ height += _nestLengthY + strokeWidth * 4 + 2 * cornerRadius + notchArgLengthY;
+ }
+
+ return {
+ extent: {
+ width: strokeWidth + _innerLengthX + strokeWidth,
+ height: height,
+ },
+ coords: {
+ x: _hasNotchArg ? notchArgLengthX : 0,
+ y: 0,
+ },
+ };
+}
+
+/**
+ * Generates bounding box of the argument notch positioned on the left
+ *
+ * @private
+ */
+function _getBBoxNotchArg(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number };
+} {
+ return {
+ extent: {
+ width: _hasNotchArg ? notchArgLengthX : 0,
+ height: _hasNotchArg ? strokeWidth + 8 + strokeWidth : 0,
+ },
+ coords: {
+ x: 0,
+ y: _hasNotchArg ? 6 : 0,
+ },
+ };
+}
+
+/**
+ * Generates bounding box of the top instruction notch
+ *
+ * @private
+ */
+function _getBBoxNotchInsTop(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number };
+} {
+ return {
+ extent: {
+ width: notchInsLengthX - 2 * strokeWidth,
+ height: notchInsLengthY,
+ },
+ coords: {
+ x:
+ strokeWidth +
+ (_hasNotchArg ? notchArgLengthX : 0) +
+ cornerRadius +
+ notchInsOffsetX +
+ strokeWidth,
+ y: 0,
+ },
+ };
+}
+
+/**
+ * Generates bounding box of the bottom instruction notch
+ *
+ * @private
+ */
+function _getBBoxNotchInsBot(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number };
+} {
+ return {
+ extent: {
+ width: notchInsLengthX - 2 * strokeWidth,
+ height: strokeWidth + notchInsLengthY - strokeWidth,
+ },
+ coords: {
+ x:
+ strokeWidth +
+ (_hasNotchArg ? notchArgLengthX : 0) +
+ cornerRadius +
+ notchInsOffsetX +
+ strokeWidth,
+ y: _getBBoxBrick().extent.height,
+ },
+ };
+}
+
+/**
+ * Generates bounding box of the top instruction notch inside a nesting
+ *
+ * @private
+ */
+function _getBBoxNotchInsNestTop(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number };
+} {
+ const argSectionLengthYMin = cornerRadius * 2 + notchArgLengthY;
+ const argsLength = _argsLengthY
+ .map((sectionLengthY) => Math.max(argSectionLengthYMin, sectionLengthY))
+ .reduce((a, b) => a + b, 0);
+
+ const offsetY =
+ strokeWidth +
+ (argsLength !== 0 ? argsLength : 2 * cornerRadius + notchArgLengthY) +
+ strokeWidth;
+
+ return {
+ extent: {
+ width: notchInsLengthX - 2 * strokeWidth,
+ height: notchInsLengthY,
+ },
+ coords: {
+ x:
+ strokeWidth +
+ (_hasNotchArg ? notchArgLengthX : 0) +
+ cornerRadius +
+ notchInsOffsetX +
+ notchInsLengthX -
+ strokeWidth,
+ y: offsetY,
+ },
+ };
+}
+
+/**
+ * Generates list of bounding boxes for each argument connector positioned on the right
+ *
+ * @private
+ */
+function _getBBoxArgs(): {
+ extent: { width: number; height: number };
+ coords: { x: number; y: number }[];
+} {
+ const offsetX =
+ strokeWidth +
+ (_hasNotchArg ? notchArgLengthX : 0) +
+ _innerLengthX -
+ notchArgLengthX +
+ strokeWidth;
+ const firstOffsetY = strokeWidth + cornerRadius + 1 + strokeWidth;
+ const argSectionLengthYMin = cornerRadius * 2 + notchArgLengthY;
+ const argsLength = _argsLengthY.map((sectionLengthY) =>
+ Math.max(argSectionLengthYMin, sectionLengthY),
+ );
+
+ return {
+ extent: {
+ width: notchArgLengthX,
+ height: 10 - 2 * strokeWidth,
+ },
+ coords: _argsLengthY.map((_, index) => {
+ return {
+ x: offsetX,
+ y:
+ firstOffsetY +
+ (index === 0 ? 0 : argsLength.slice(0, index).reduce((a, b) => a + b, 0)),
+ };
+ }),
+ };
+}
+
+// == public functions =============================================================================
+
+/**
+ * Generates SVG path along with information about the bounding boxes of notches and arguments.
+ *
+ * @remarks
+ * Use https://yqnn.github.io/svg-path-editor/ to visualize the path
+ *
+ * @param options determines how the path looks
+ * @param print whether to print results in the console (only to be used during development)
+ */
+export function generatePath(
+ options:
+ | {
+ hasNest: true;
+ hasNotchArg: boolean;
+ hasNotchInsTop: boolean;
+ hasNotchInsBot: boolean;
+ scale: number;
+ nestLengthY: number;
+ innerLengthX: number;
+ argHeights: number[];
+ }
+ | {
+ hasNest: false;
+ hasNotchArg: boolean;
+ hasNotchInsTop: boolean;
+ hasNotchInsBot: boolean;
+ scale: number;
+ innerLengthX: number;
+ argHeights: number[];
+ },
+ print?: boolean,
+): {
+ /** path definition commands string */
+ path: string;
+ /** bounding box of the brick (actual area of the brick excluding notches) */
+ bBoxBrick: {
+ /** width and height of the brick */
+ extent: { width: number; height: number };
+ /** x and y co-ordinates of the brick relative to the origin of the SVG */
+ coords: { x: number; y: number };
+ };
+ /** bounding box of the argument notch (on the left) */
+ bBoxNotchArg: {
+ /** width and height of the argument notch */
+ extent: { width: number; height: number };
+ /** x and y co-ordinates of the argument notch relative to the origin of the SVG */
+ coords: { x: number; y: number };
+ } | null;
+ /** bounding box of the top instruction notch */
+ bBoxNotchInsTop: {
+ /** width and height of the top instruction notch */
+ extent: { width: number; height: number };
+ /** x and y co-ordinates of the top instruction notch relative to the origin of the SVG */
+ coords: { x: number; y: number };
+ } | null;
+ /** bounding box of the bottom instruction notch */
+ bBoxNotchInsBot: {
+ /** width and height of the bottom instruction notch */
+ extent: { width: number; height: number };
+ /** x and y co-ordinates of the bottom instruction notch relative to the origin of the SVG */
+ coords: { x: number; y: number };
+ } | null;
+ /** bounding box of the top instruction notch inside a nest (only for bricks with nesting) */
+ bBoxNotchInsNestTop: {
+ /** width and height of the top instruction notch inside a nest */
+ extent: { width: number; height: number };
+ /** x and y co-ordinates of the top instruction notch inside a nest relative to the origin of the SVG */
+ coords: { x: number; y: number };
+ } | null;
+ /** list of bounding boxes for the argument connections */
+ bBoxArgs: {
+ /** width and height of each argument connection */
+ extent: { width: number; height: number };
+ /** list of x and y co-ordinates of each argument connection relative to the origin of the SVG */
+ coords: { x: number; y: number }[];
+ };
+} {
+ _setOptions(options);
+
+ const results = {
+ path: _getPath(),
+ bBoxBrick: _getBBoxBrick(),
+ bBoxNotchArg: _getBBoxNotchArg(),
+ bBoxNotchInsTop: _getBBoxNotchInsTop(),
+ bBoxNotchInsBot: _getBBoxNotchInsBot(),
+ bBoxNotchInsNestTop: _getBBoxNotchInsNestTop(),
+ bBoxArgs: _getBBoxArgs(),
+ };
+
+ if (print || import.meta.env.DEV) console.log(results);
+
+ return results;
+}
diff --git a/modules/masonry/src/brick/utils/spec/path.spec.ts b/modules/masonry/src/brick/utils/spec/path.spec.ts
new file mode 100644
index 00000000..92dd5d07
--- /dev/null
+++ b/modules/masonry/src/brick/utils/spec/path.spec.ts
@@ -0,0 +1,529 @@
+import { generatePath } from '../path';
+
+describe('Masonry: Brick > Design 0 > Utility: Path', () => {
+ describe('Path Generation', () => {
+ it('generates path with arguments of different extents', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -0 v -12 z',
+ );
+ }
+
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -12 z',
+ );
+ }
+
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -40 v -12 z',
+ );
+ }
+
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [40, 20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 20 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -60 v -12 z',
+ );
+ }
+
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 40, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 20 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -60 v -12 z',
+ );
+ }
+
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20, 40],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 20 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -60 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with argument notch and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 8.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -0 v -5 h -6 v 3 h -2 v -8 h 2 v 3 h 6 v -5 z',
+ );
+ }
+ });
+
+ it('generates path with argument notch and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 8.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -5 h -6 v 3 h -2 v -8 h 2 v 3 h 6 v -5 z',
+ );
+ }
+ });
+
+ it('generates path with both instruction notches and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -78 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with both instruction notches and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with top instruction notch and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with top instruction notch and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with bottom instruction notch and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -78 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with bottom instruction notch and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -78 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, both instruction notches, and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, both instruction notches, and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, top instruction notch, and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, top instruction notch, and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 v 2 h 10 v -2 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, bottom instruction notch, and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, bottom instruction notch, and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -1 v 2 h -8 v -2 h -1 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -20 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, no instruction notch, and no arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -0 v -12 z',
+ );
+ }
+ });
+
+ it('generates path with nesting, no instruction notch, and arguments', () => {
+ {
+ const { path } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ nestLengthY: 40,
+ argHeights: [20, 20],
+ });
+ expect(path).toBe(
+ 'm 0.5 4.5 a 4 4 90 0 1 4 -4 h 4 h 10 h 78 a 4 4 90 0 1 4 4 v 0 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 4 v 0 v 4 v 4 h -4 v -3 h -4 v 10 h 4 v -3 h 4 v 4 v 0 v 0 a 4 4 90 0 1 -4 4 h -69 h -1 v 2 h -8 v -2 h -1 h -4 a 5 5 90 0 0 -5 5 v 32 a 5 5 90 0 0 5 5 h 4 v 2 h 10 v -2 h 4 a 4 4 90 0 1 4 4 v 12 a 4 4 90 0 1 -4 4 h -1 h -12 h -10 h -4 a 4 4 90 0 1 -4 -4 v -20 v -1 v -32 v -1 v -8 v -20 v -12 z',
+ );
+ }
+ });
+ });
+
+ describe('Bounding Box Calculation', () => {
+ it('evaluates brick bounding box for brick with no argument notch', () => {
+ const { bBoxBrick } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxBrick.extent.height).toBe(21);
+ expect(bBoxBrick.extent.width).toBe(101);
+ expect(bBoxBrick.coords.x).toBe(0);
+ expect(bBoxBrick.coords.y).toBe(0);
+ });
+
+ it('evaluates brick bounding box for brick with argument notch', () => {
+ const { bBoxBrick } = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxBrick.extent.height).toBe(21);
+ expect(bBoxBrick.extent.width).toBe(101);
+ expect(bBoxBrick.coords.x).toBe(8);
+ expect(bBoxBrick.coords.y).toBe(0);
+ });
+
+ it('evaluates argument notch bounding box for brick', () => {
+ const { bBoxNotchArg } = generatePath({
+ hasNest: false,
+ hasNotchArg: true,
+ hasNotchInsTop: false,
+ hasNotchInsBot: false,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchArg?.extent.height).toBe(9);
+ expect(bBoxNotchArg?.extent.width).toBe(8);
+ expect(bBoxNotchArg?.coords.x).toBe(0);
+ expect(bBoxNotchArg?.coords.y).toBe(6);
+ });
+
+ it('evaluates top instruction notch bounding box', () => {
+ const { bBoxNotchInsTop } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchInsTop?.extent.height).toBe(2);
+ expect(bBoxNotchInsTop?.extent.width).toBe(9);
+ expect(bBoxNotchInsTop?.coords.x).toBe(9);
+ expect(bBoxNotchInsTop?.coords.y).toBe(0);
+ });
+
+ it('evaluates bottom instruction notch bounding box for non-nesting brick', () => {
+ const { bBoxNotchInsBot } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchInsBot?.extent.height).toBe(2);
+ expect(bBoxNotchInsBot?.extent.width).toBe(9);
+ expect(bBoxNotchInsBot?.coords.x).toBe(9);
+ expect(bBoxNotchInsBot?.coords.y).toBe(21);
+ });
+
+ it('evaluates bottom instruction notch bounding box for nesting brick', () => {
+ const { bBoxNotchInsBot } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ nestLengthY: 30,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchInsBot?.extent.height).toBe(2);
+ expect(bBoxNotchInsBot?.extent.width).toBe(9);
+ expect(bBoxNotchInsBot?.coords.x).toBe(9);
+ expect(bBoxNotchInsBot?.coords.y).toBe(73);
+ });
+
+ it('evaluates inner top instruction notch bounding box for nesting brick', () => {
+ const { bBoxNotchInsNestTop } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ nestLengthY: 30,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchInsNestTop?.extent.height).toBe(2);
+ expect(bBoxNotchInsNestTop?.extent.width).toBe(9);
+ expect(bBoxNotchInsNestTop?.coords.x).toBe(18);
+ expect(bBoxNotchInsNestTop?.coords.y).toBe(21);
+ });
+
+ it('evaluates inner top instruction notch bounding box for nesting brick', () => {
+ const { bBoxNotchInsNestTop } = generatePath({
+ hasNest: true,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ nestLengthY: 30,
+ innerLengthX: 100,
+ argHeights: [],
+ });
+ expect(bBoxNotchInsNestTop?.extent.height).toBe(2);
+ expect(bBoxNotchInsNestTop?.extent.width).toBe(9);
+ expect(bBoxNotchInsNestTop?.coords.x).toBe(18);
+ expect(bBoxNotchInsNestTop?.coords.y).toBe(21);
+ });
+
+ it('evaluates bounding boxes for arguments', () => {
+ const { bBoxArgs } = generatePath({
+ hasNest: false,
+ hasNotchArg: false,
+ hasNotchInsTop: true,
+ hasNotchInsBot: true,
+ scale: 1,
+ innerLengthX: 100,
+ argHeights: [17, 30, 40],
+ });
+ expect(bBoxArgs.extent.height).toBe(9);
+ expect(bBoxArgs.extent.width).toBe(8);
+ expect(bBoxArgs.coords).toStrictEqual([
+ { x: 93, y: 6 },
+ { x: 93, y: 26 },
+ { x: 93, y: 56 },
+ ]);
+ });
+ });
+});
diff --git a/modules/masonry/src/index.ts b/modules/masonry/src/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/masonry/src/stack/README.md b/modules/masonry/src/stack/README.md
new file mode 100644
index 00000000..3c208712
--- /dev/null
+++ b/modules/masonry/src/stack/README.md
@@ -0,0 +1,62 @@
+# Data Model for Stack Tree
+
+## Stack Node
+
+### Intrinsic
+
+- `brick`: reference to the BrickModel (Data, Expression, Statement, Block) instance
+- `children`: array of child StackNode instances
+
+### Methods
+
+- `constructor(brick: BrickModelData | BrickModelExpression | BrickModelStatement |BrickModelBlock)`
+: Initializes a StackNode with the given brick and an empty children array.
+
+## Stack
+
+### - Intrinsic
+
+- `id`: unique ID for the stack instance
+- `rootNodes`: array of root StackNode instances
+- `_validationDisabled`: private flag to disable validation checks
+
+### - Methods
+
+- `constructor(id: string)`: Initializes a Stack with the given ID and an empty rootNodes array.
+- `validate(): boolean`: Validates the stack, returning `true` if there are no validation errors or
+if validation is disabled.
+- `addNode(node: IStackNode, parentId?: string): void`: Adds a node to the stack. If `parentId` is
+provided, the node is added as a child of the specified parent node.
+- `removeNode(id: string): void`: Removes a node from the stack by its ID.
+- `moveNode(id: string, newParentId: string, newIndex: number): void`: Moves a node to a new parent
+ node at the specified index.
+- `collapse(id: string): void`: Collapses a block node, hiding its children.
+- `expand(id: string): void`: Expands a block node, showing its children.
+- `getValidationErrors(): string[]`: Returns an array of validation error messages.
+- `disableValidation(): void`: Disables validation checks.
+- `enableValidation(): void`: Enables validation checks.
+
+### Private Methods
+
+- `findNode(id: string): IStackNode | null`: Finds a node by its ID.
+- `updateNestExtent(node: IStackNode): void`: Updates the nesting extent of a block node based on
+ its children.
+- `isValidConnection(node: IStackNode, position: 'above' | 'below'): boolean`: Checks if a connection
+at the specified position is valid for the given node.
+
+## Factory Function
+
+### -Methods
+
+- `createStackNode(brick: BrickModelData | BrickModelExpression | BrickModelStatement |
+BrickModelBlock): IStackNode`: Creates a StackNode based on the given brick type.
+
+---
+
+**Note:** Intrinsic properties are set in the constructor and cannot be modified once instantiated.
+They are accessible using getters.
+
+**Note:** Private methods are for internal use within the Stack class and should not be accessed
+directly from outside.
+
+---
diff --git a/modules/masonry/src/stack/data.ts b/modules/masonry/src/stack/data.ts
new file mode 100644
index 00000000..1d1e2217
--- /dev/null
+++ b/modules/masonry/src/stack/data.ts
@@ -0,0 +1,318 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import {
+ BrickModelData,
+ BrickModelExpression,
+ BrickModelStatement,
+ BrickModelBlock,
+} from '../brick/model';
+
+/**
+ * @interface IStackNode
+ * Represents a node in the stack structure.
+ */
+export interface IStackNode {
+ /** The brick model associated with this node */
+ brick: BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock;
+ /** Child nodes of this node */
+ children: IStackNode[];
+}
+
+/**
+ * @interface IStack
+ * Represents the stack structure for managing bricks.
+ */
+export interface IStack {
+ /** Unique identifier for the stack */
+ id: string;
+ /** Root nodes of the stack */
+ rootNodes: IStackNode[];
+
+ /**
+ * Validates the entire stack structure.
+ * @returns {boolean} True if the stack is valid, false otherwise.
+ */
+ validate(): boolean;
+
+ /**
+ * Adds a new node to the stack.
+ * @param {IStackNode} node - The node to add.
+ * @param {string} [parentId] - The ID of the parent node (optional).
+ */
+ addNode(node: IStackNode, parentId?: string): void;
+
+ /**
+ * Removes a node from the stack.
+ * @param {string} id - The ID of the node to remove.
+ */
+ removeNode(id: string): void;
+
+ /**
+ * Moves a node to a new position in the stack.
+ * @param {string} id - The ID of the node to move.
+ * @param {string} newParentId - The ID of the new parent node.
+ * @param {number} newIndex - The new index position under the parent.
+ */
+ moveNode(id: string, newParentId: string, newIndex: number): void;
+
+ /**
+ * Collapses a block node.
+ * @param {string} id - The ID of the node to collapse.
+ */
+ collapse(id: string): void;
+
+ /**
+ * Expands a block node.
+ * @param {string} id - The ID of the node to expand.
+ */
+ expand(id: string): void;
+
+ /**
+ * Gets all validation errors in the stack.
+ * @returns {string[]} An array of error messages.
+ */
+ getValidationErrors(): string[];
+}
+
+/**
+ * @class StackNode
+ * Implements the IStackNode interface.
+ */
+class StackNode implements IStackNode {
+ brick: BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock;
+ children: IStackNode[];
+
+ /**
+ * Creates a new StackNode.
+ * @param {BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock} brick - The brick model for this node.
+ */
+ constructor(
+ brick: BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock,
+ ) {
+ this.brick = brick;
+ this.children = [];
+ }
+}
+
+/**
+ * @class Stack
+ * Implements the IStack interface.
+ */
+class Stack implements IStack {
+ id: string;
+ rootNodes: IStackNode[];
+ private _validationDisabled = false;
+
+ /**
+ * Creates a new Stack.
+ * @param {string} id - The unique identifier for this stack.
+ */
+ constructor(id: string) {
+ this.id = id;
+ this.rootNodes = [];
+ }
+
+ validate(): boolean {
+ if (this._validationDisabled) return true;
+ return this.getValidationErrors().length === 0;
+ }
+
+ addNode(node: IStackNode, parentId?: string): void {
+ if (!parentId) {
+ this.rootNodes.push(node);
+ } else {
+ const parent = this.findNode(parentId);
+ if (
+ parent &&
+ (parent.brick instanceof BrickModelBlock ||
+ parent.brick instanceof BrickModelExpression)
+ ) {
+ parent.children.push(node);
+ this.updateNestExtent(parent);
+ } else {
+ throw new Error('Parent node not found or cannot have children');
+ }
+ }
+ }
+
+ removeNode(id: string): void {
+ const remove = (nodes: IStackNode[]): boolean => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].brick.uuid === id) {
+ nodes.splice(i, 1);
+ return true;
+ }
+ if (nodes[i].children.length > 0 && remove(nodes[i].children)) {
+ this.updateNestExtent(nodes[i]);
+ return true;
+ }
+ }
+ return false;
+ };
+ remove(this.rootNodes);
+ }
+
+ moveNode(id: string, newParentId: string, newIndex: number): void {
+ const node = this.findNode(id);
+ if (!node) throw new Error('Node not found');
+
+ this.removeNode(id);
+ const newParent = this.findNode(newParentId);
+ if (!newParent) throw new Error('New parent node not found');
+
+ if (
+ !(newParent.brick instanceof BrickModelBlock) &&
+ !(newParent.brick instanceof BrickModelExpression)
+ ) {
+ throw new Error('New parent cannot have children');
+ }
+
+ newParent.children.splice(newIndex, 0, node);
+ this.updateNestExtent(newParent);
+ }
+
+ collapse(id: string): void {
+ const node = this.findNode(id);
+ if (node && node.brick instanceof BrickModelBlock) {
+ node.brick.collapsed = true;
+ this.updateNestExtent(node);
+ }
+ }
+
+ expand(id: string): void {
+ const node = this.findNode(id);
+ if (node && node.brick instanceof BrickModelBlock) {
+ node.brick.collapsed = false;
+ this.updateNestExtent(node);
+ }
+ }
+
+ getValidationErrors(): string[] {
+ const errors: string[] = [];
+ const validateNode = (node: IStackNode) => {
+ // Check connection compatibility
+ if (
+ node.brick instanceof BrickModelStatement ||
+ node.brick instanceof BrickModelBlock
+ ) {
+ if (node.brick.connectAbove && !this.isValidConnection(node, 'above')) {
+ errors.push(`Invalid connection above for node ${node.brick.uuid}`);
+ }
+ if (node.brick.connectBelow && !this.isValidConnection(node, 'below')) {
+ errors.push(`Invalid connection below for node ${node.brick.uuid}`);
+ }
+ }
+
+ // Check argument data type compatibility
+ if (
+ node.brick instanceof BrickModelExpression ||
+ node.brick instanceof BrickModelStatement ||
+ node.brick instanceof BrickModelBlock
+ ) {
+ for (const [argId, arg] of Object.entries(node.brick.args)) {
+ const childNode = node.children.find((child) => child.brick.uuid === argId);
+ if (childNode && 'dataType' in childNode.brick) {
+ if (childNode.brick.dataType !== arg.dataType && arg.dataType !== 'any') {
+ errors.push(
+ `Data type mismatch for argument ${argId} in node ${node.brick.uuid}`,
+ );
+ }
+ }
+ }
+ }
+
+ // Recursively validate children
+ node.children.forEach(validateNode);
+ };
+
+ this.rootNodes.forEach(validateNode);
+ return errors;
+ }
+
+ /**
+ * Disables validation for this stack.
+ */
+ disableValidation(): void {
+ this._validationDisabled = true;
+ }
+
+ /**
+ * Enables validation for this stack.
+ */
+ enableValidation(): void {
+ this._validationDisabled = false;
+ }
+
+ /**
+ * Finds a node in the stack by its ID.
+ * @param {string} id - The ID of the node to find.
+ * @returns {IStackNode | null} The found node or null if not found.
+ */
+ private findNode(id: string): IStackNode | null {
+ const find = (nodes: IStackNode[]): IStackNode | null => {
+ for (const node of nodes) {
+ if (node.brick.uuid === id) return node;
+ if (node.children.length > 0) {
+ const found = find(node.children);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+ return find(this.rootNodes);
+ }
+
+ /**
+ * Updates the nest extent of a block node.
+ * @param {IStackNode} node - The node to update.
+ */
+ private updateNestExtent(node: IStackNode): void {
+ if (node.brick instanceof BrickModelBlock) {
+ const childrenExtent = node.children.reduce(
+ (acc, child) => {
+ const childExtent = child.brick.bBoxBrick.extent;
+ return {
+ width: Math.max(acc.width, childExtent.width),
+ height: acc.height + childExtent.height,
+ };
+ },
+ { width: 0, height: 0 },
+ );
+
+ node.brick.nestExtent = childrenExtent;
+ }
+ }
+
+ /**
+ * Checks if a connection is valid for a given node and position.
+ * @param {IStackNode} node - The node to check.
+ * @param {'above' | 'below'} position - The position to check.
+ * @returns {boolean} True if the connection is valid, false otherwise.
+ */
+ private isValidConnection(node: IStackNode, position: 'above' | 'below'): boolean {
+ if (!(node.brick instanceof BrickModelStatement || node.brick instanceof BrickModelBlock)) {
+ return false;
+ }
+
+ if (position === 'above') {
+ return node.brick.connectAbove;
+ } else if (position === 'below') {
+ return node.brick.connectBelow;
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Creates a StackNode based on the provided brick model.
+ * @param {BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock} brick - The brick model for the node.
+ * @returns {IStackNode} A new StackNode instance.
+ */
+function createStackNode(
+ brick: BrickModelData | BrickModelExpression | BrickModelStatement | BrickModelBlock,
+): IStackNode {
+ return new StackNode(brick);
+}
+
+// Export the Stack class and createStackNode function
+export { Stack, createStackNode };
diff --git a/modules/masonry/src/stack/index.ts b/modules/masonry/src/stack/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/masonry/tsconfig.json b/modules/masonry/tsconfig.json
new file mode 100644
index 00000000..737827ed
--- /dev/null
+++ b/modules/masonry/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": [
+ "./**/*.ts",
+ "./**/*.tsx"
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ],
+ "#/@types/*": [
+ "../../@types/*"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8e2766f7..daa6c170 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"modules/menu",
"modules/painter",
"modules/singer",
+ "modules/masonry",
"app"
],
"dependencies": {
@@ -167,6 +168,28 @@
"react-dom": "~18.x"
}
},
+ "modules/masonry": {
+ "name": "@sugarlabs/mb4-module-masonry",
+ "version": "4.2.0",
+ "dependencies": {
+ "uuid": "^10.0.0"
+ },
+ "devDependencies": {
+ "@types/uuid": "^10.0.0"
+ }
+ },
+ "modules/masonry/node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"modules/menu": {
"name": "@sugarlabs/mb4-module-menu",
"version": "4.2.0",
@@ -9231,6 +9254,10 @@
"resolved": "modules/editor",
"link": true
},
+ "node_modules/@sugarlabs/mb4-module-masonry": {
+ "resolved": "modules/masonry",
+ "link": true
+ },
"node_modules/@sugarlabs/mb4-module-menu": {
"resolved": "modules/menu",
"link": true
@@ -10207,6 +10234,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true
+ },
"node_modules/@types/yargs": {
"version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
diff --git a/package.json b/package.json
index 9c609287..cd0bb8ee 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
"modules/menu",
"modules/painter",
"modules/singer",
+ "modules/masonry",
"app"
],
"dependencies": {