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. + + ![alt text](../images/image.png) +- 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. + + ![editable bricks](../images/image-1.png) + +#### 2. Expression Bricks: Takes values as input, returns a value as output + + ![alt text](../images/image-2.png) + +#### 3. Statement Bricks: These define actions to be taken + + ![example](../images/image-3.png) + +#### 4. Block Bricks: Contain nesting, also execute something like the statement + +#### bricks. Takes 0 or more arguments + + ![alt text](../images/image-14.png) + +### 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. + + ![inline text editing](../images/image-4.png) + + - Other bricks open both an inline text editor and a context menu for additional options. + + ![inline text editor with context menu](../images/image-15.png) +- **Context Menus**: For more complex properties, a separate interface allows detailed configuration. + + ![dedicated editors](../images/image-5.png) +- **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. + + ![alt text](../images/image-8.png) + +## 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. + + ![alt text](../images/image-7.png) + +- **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. + + ![alt text](../images/image-9.png) + +## 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 + + ![alt text](../images/image-10.png) + +### b. **Brick Previews** + +- Tooltips or pop-ups displaying Brick names and brief descriptions + + ![alt text](../images/image-16.png) + +- 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: + + ![alt text](../images//image-11.png) + + - Searchbar design to be implemented: + + ![alt text](../images/image-17.png) + 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. + + ![drag and drop](../images/image-12.png) + + - While dragging a brick from the palette, the brick should temporarily disappear from the palette + until it is placed in the workspace. + +## 4. Workspace + +![alt text](../images/image-13.png) + +- **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) => ( + + {workspace.data.map((brick) => { + return ; + })} + + ))} +
+ ); +} + +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 ( + + {props.children} + + ); +} 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": {