Modular Universe Evolution - Cutting Mechanic & Storage Design

Complete specification for canvas cutting mechanic and storage architecture

✨ Modular Universe Evolution - Cutting Mechanic & Storage Design

Last Updated: November 14, 2025
Status: 🎯 MASTER SPECIFICATION
Purpose: Revolutionary canvas cutting mechanic implementation plan


🎯 EXECUTIVE SUMMARY

This document specifies the revolutionary canvas cutting mechanic that will make modular-universe the most powerful, intuitive universe builder on the planet.

Vision: Beautiful empty canvas → User adds divider lines → Drag to position → Confirm → Sections auto-generated → Each section becomes a module

Current State: ✅ Foundation exists (grid-canvas with modes)
Next Evolution: ⚡ Add visual cutting mechanic with divider lines


📊 CURRENT STATE REVIEW

What's Already Implemented ✅

From grid-canvas.component.ts:

// Mode system
type CanvasMode = 'select' | 'add' | 'resize' | 'reorder';

// Layout support
interface LayoutConfiguration {
  type: 'flexbox' | 'grid';
  flexbox?: FlexboxConfig;
  grid?: GridConfig;
  sections: SectionConfig[];
}

// Section management
- addSection(position?, referenceId?)
- removeSection(sectionId)
- updateSection(section)
- resizeSection(sectionId, handle, dimensions)
- moveSectionUpDown(sectionId, direction)

// Storage integration
- loadLayoutConfiguration()
- saveLayoutConfiguration()

From section-config-panel.component.ts:

// Section configuration
interface SectionConfig {
  id: string;
  name: string;
  contentType: 'empty' | 'module' | 'page' | 'widget-grid' | 'custom';
  assignedModuleId?: string;
  flexbox?: FlexboxSectionConfig;
  grid?: GridSectionConfig;
  positioning: PositioningConfig;
  behavior: BehaviorConfig;
  visibility: VisibilityConfig;
  // ... more
}

From modular-universe-builder.component.ts:

// Orchestrator
- Combines grid-canvas + section-config-panel
- Manages module assignment
- Handles universe/galaxy context

What's Missing ⚠️

  1. Divider Lines - Visual horizontal/vertical lines
  2. Cutting UX - Drag dividers to slice canvas
  3. Auto-Section Generation - Calculate sections from dividers
  4. Template Presets - Common layouts (header/content/footer)
  5. Visual Feedback - Show section boundaries during cutting

🎨 THE CUTTING MECHANIC

User Experience Flow

STEP 1: Empty Canvas
┌─────────────────────────────────────────┐
│                                         │
│                                         │
│        Empty Canvas                     │
│        Click to add dividers            │
│                                         │
│                                         │
└─────────────────────────────────────────┘

STEP 2: Add Horizontal Divider
┌─────────────────────────────────────────┐
│                                         │
│                                         │
├─────────────────────────────────────────┤ ← Draggable line
│                                         │
│                                         │
│                                         │
└─────────────────────────────────────────┘

STEP 3: Add Vertical Divider
┌──────────────────┬──────────────────────┐
│                  │                      │
│                  │                      │
├──────────────────┼──────────────────────┤
│                  │                      │
│                  │                      │
│                  │                      │
└──────────────────┴──────────────────────┘

STEP 4: Confirm → Sections Generated
┌──────────────────┬──────────────────────┐
│   Section 1      │    Section 2         │
│   (80px fixed)   │    (1fr flexible)    │
├──────────────────┼──────────────────────┤
│   Section 3      │    Section 4         │
│   (1fr flexible) │    (1fr flexible)    │
│                  │                      │
└──────────────────┴──────────────────────┘

Each section = Module with configurable behavior

Mode Addition

New Mode: 'divide'

// Updated canvas-mode.types.ts
export type CanvasMode = 
  | 'select'    // Select and configure sections
  | 'add'       // Add new sections
  | 'resize'    // Resize sections
  | 'reorder'   // Reorder sections
  | 'divide';   // NEW: Add divider lines to cut canvas

export const CANVAS_MODES: Record<CanvasMode, CanvasModeConfig> = {
  // ... existing modes
  
  divide: {
    id: 'divide',
    name: 'Divide Canvas',
    description: 'Add horizontal or vertical lines to slice the canvas into sections',
    icon: 'cut',
    cursor: 'crosshair',
    allowSelection: false,
    allowResize: false,
    allowReorder: false,
    allowDivide: true  // NEW property
  }
};

Divider Data Model

// models/divider.types.ts

export interface Divider {
  id: string;
  type: 'horizontal' | 'vertical';
  position: number;  // Pixels from top (horizontal) or left (vertical)
  positionPercent: number;  // 0-100 for responsive
  isDragging: boolean;
  isTemporary: boolean;  // True until confirmed
  createdAt: Date;
}

export interface DividerState {
  horizontal: Divider[];
  vertical: Divider[];
  isActive: boolean;  // True when in divide mode
  canConfirm: boolean;  // True when valid layout
}

export interface GeneratedSection {
  id: string;
  name: string;
  bounds: {
    top: number;
    left: number;
    width: number;
    height: number;
  };
  gridArea?: {
    row: number;
    col: number;
    rowSpan: number;
    colSpan: number;
  };
  flexbox?: {
    flexGrow: number;
    flexShrink: number;
    flexBasis: string;
  };
}

🛠️ IMPLEMENTATION SPECIFICATION

Component: Divider Tool

// components/divider-tool/divider-tool.component.ts

@Component({
  selector: 'lib-divider-tool',
  standalone: true,
  imports: [CommonModule, CdkDrag],
  template: `
    <div 
      class="divider-line"
      [class.horizontal]="type === 'horizontal'"
      [class.vertical]="type === 'vertical'"
      [class.dragging]="isDragging"
      [style.top.px]="type === 'horizontal' ? position : 0"
      [style.left.px]="type === 'vertical' ? position : 0"
      [style.width]="type === 'horizontal' ? '100%' : '2px'"
      [style.height]="type === 'vertical' ? '100%' : '2px'"
      cdkDrag
      [cdkDragBoundary]="'.canvas-container'"
      (cdkDragStarted)="onDragStart()"
      (cdkDragMoved)="onDragMove($event)"
      (cdkDragEnded)="onDragEnd()"
    >
      <div class="divider-handle">
        <span class="divider-label">{{ position }}px</span>
        <button (click)="onRemove.emit(divider.id)" class="remove-btn">×</button>
      </div>
    </div>
  `,
  styleUrls: ['./divider-tool.component.scss']
})
export class DividerToolComponent {
  @Input({ required: true }) divider!: Divider;
  @Input({ required: true }) canvasSize!: { width: number; height: number };
  
  @Output() positionChanged = new EventEmitter<{ id: string; position: number }>();
  @Output() onRemove = new EventEmitter<string>();
  
  isDragging = signal(false);
  
  get type() {
    return this.divider.type;
  }
  
  get position() {
    return this.divider.position;
  }
  
  onDragStart() {
    this.isDragging.set(true);
  }
  
  onDragMove(event: CdkDragMove) {
    const delta = this.type === 'horizontal' 
      ? event.distance.y 
      : event.distance.x;
    
    const maxPosition = this.type === 'horizontal'
      ? this.canvasSize.height
      : this.canvasSize.width;
    
    const newPosition = Math.max(50, Math.min(maxPosition - 50, this.position + delta));
    
    this.positionChanged.emit({
      id: this.divider.id,
      position: newPosition
    });
  }
  
  onDragEnd() {
    this.isDragging.set(false);
  }
}

Service: Divider Manager

// services/divider-manager.service.ts

@Injectable()
export class DividerManagerService {
  
  /**
   * Add divider line to canvas
   */
  addDivider(
    type: 'horizontal' | 'vertical',
    position: number,
    canvasSize: { width: number; height: number }
  ): Divider {
    const divider: Divider = {
      id: `divider_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      type,
      position,
      positionPercent: this.calculatePercent(position, type, canvasSize),
      isDragging: false,
      isTemporary: true,
      createdAt: new Date()
    };
    
    return divider;
  }
  
  /**
   * Update divider position
   */
  updateDividerPosition(
    divider: Divider,
    newPosition: number,
    canvasSize: { width: number; height: number }
  ): Divider {
    return {
      ...divider,
      position: newPosition,
      positionPercent: this.calculatePercent(newPosition, divider.type, canvasSize)
    };
  }
  
  /**
   * Generate sections from dividers
   */
  generateSectionsFromDividers(
    dividers: DividerState,
    canvasSize: { width: number; height: number },
    layoutType: 'flexbox' | 'grid'
  ): GeneratedSection[] {
    const horizontal = [...dividers.horizontal].sort((a, b) => a.position - b.position);
    const vertical = [...dividers.vertical].sort((a, b) => a.position - b.position);
    
    if (layoutType === 'grid') {
      return this.generateGridSections(horizontal, vertical, canvasSize);
    } else {
      return this.generateFlexboxSections(horizontal, vertical, canvasSize);
    }
  }
  
  /**
   * Generate CSS Grid sections
   */
  private generateGridSections(
    horizontal: Divider[],
    vertical: Divider[],
    canvasSize: { width: number; height: number }
  ): GeneratedSection[] {
    const sections: GeneratedSection[] = [];
    
    // Calculate row heights
    const rows = this.calculateGridRows(horizontal, canvasSize.height);
    
    // Calculate column widths
    const columns = this.calculateGridColumns(vertical, canvasSize.width);
    
    // Generate section for each grid cell
    let sectionIndex = 0;
    for (let row = 0; row < rows.length; row++) {
      for (let col = 0; col < columns.length; col++) {
        sectionIndex++;
        
        const section: GeneratedSection = {
          id: `section_${Date.now()}_${sectionIndex}`,
          name: `Section ${sectionIndex}`,
          bounds: {
            top: this.calculateRowStart(row, rows, horizontal),
            left: this.calculateColStart(col, columns, vertical),
            width: columns[col],
            height: rows[row]
          },
          gridArea: {
            row: row + 1,
            col: col + 1,
            rowSpan: 1,
            colSpan: 1
          }
        };
        
        sections.push(section);
      }
    }
    
    return sections;
  }
  
  /**
   * Generate Flexbox sections
   */
  private generateFlexboxSections(
    horizontal: Divider[],
    vertical: Divider[],
    canvasSize: { width: number; height: number }
  ): GeneratedSection[] {
    // For flexbox, sections are created sequentially
    // Use horizontal dividers to determine section order
    const sections: GeneratedSection[] = [];
    const sortedDividers = horizontal.sort((a, b) => a.position - b.position);
    
    let previousPosition = 0;
    sortedDividers.forEach((divider, index) => {
      const section: GeneratedSection = {
        id: `section_${Date.now()}_${index + 1}`,
        name: `Section ${index + 1}`,
        bounds: {
          top: previousPosition,
          left: 0,
          width: canvasSize.width,
          height: divider.position - previousPosition
        },
        flexbox: {
          flexGrow: 0,
          flexShrink: 0,
          flexBasis: `${divider.position - previousPosition}px`
        }
      };
      
      sections.push(section);
      previousPosition = divider.position;
    });
    
    // Add final section
    if (previousPosition < canvasSize.height) {
      sections.push({
        id: `section_${Date.now()}_${sections.length + 1}`,
        name: `Section ${sections.length + 1}`,
        bounds: {
          top: previousPosition,
          left: 0,
          width: canvasSize.width,
          height: canvasSize.height - previousPosition
        },
        flexbox: {
          flexGrow: 1,
          flexShrink: 1,
          flexBasis: 'auto'
        }
      });
    }
    
    return sections;
  }
  
  /**
   * Calculate grid rows from horizontal dividers
   */
  private calculateGridRows(dividers: Divider[], totalHeight: number): number[] {
    if (dividers.length === 0) return [totalHeight];
    
    const rows: number[] = [];
    let previousPosition = 0;
    
    dividers.forEach(divider => {
      rows.push(divider.position - previousPosition);
      previousPosition = divider.position;
    });
    
    // Add final row
    rows.push(totalHeight - previousPosition);
    
    return rows;
  }
  
  /**
   * Calculate grid columns from vertical dividers
   */
  private calculateGridColumns(dividers: Divider[], totalWidth: number): number[] {
    if (dividers.length === 0) return [totalWidth];
    
    const columns: number[] = [];
    let previousPosition = 0;
    
    dividers.forEach(divider => {
      columns.push(divider.position - previousPosition);
      previousPosition = divider.position;
    });
    
    // Add final column
    columns.push(totalWidth - previousPosition);
    
    return columns;
  }
  
  /**
   * Calculate percentage position
   */
  private calculatePercent(
    position: number,
    type: 'horizontal' | 'vertical',
    canvasSize: { width: number; height: number }
  ): number {
    const max = type === 'horizontal' ? canvasSize.height : canvasSize.width;
    return (position / max) * 100;
  }
}

Enhanced Grid Canvas Component

// grid-canvas.component.ts (additions)

export class GridCanvasComponent {
  // ... existing code
  
  // NEW: Divider state
  readonly dividerState = signal<DividerState>({
    horizontal: [],
    vertical: [],
    isActive: false,
    canConfirm: false
  });
  
  readonly activeDividers = computed(() => {
    const state = this.dividerState();
    return [...state.horizontal, ...state.vertical];
  });
  
  private dividerManager = inject(DividerManagerService);
  
  /**
   * Enter divide mode
   */
  enterDivideMode(): void {
    this.setCanvasMode('divide');
    this.dividerState.update(state => ({
      ...state,
      isActive: true
    }));
  }
  
  /**
   * Add horizontal divider
   */
  addHorizontalDivider(): void {
    if (this.canvasMode() !== 'divide') return;
    
    const canvasSize = this.getCanvasSize();
    const position = canvasSize.height / 2; // Default to middle
    
    const divider = this.dividerManager.addDivider('horizontal', position, canvasSize);
    
    this.dividerState.update(state => ({
      ...state,
      horizontal: [...state.horizontal, divider],
      canConfirm: true
    }));
  }
  
  /**
   * Add vertical divider
   */
  addVerticalDivider(): void {
    if (this.canvasMode() !== 'divide') return;
    
    const canvasSize = this.getCanvasSize();
    const position = canvasSize.width / 2; // Default to middle
    
    const divider = this.dividerManager.addDivider('vertical', position, canvasSize);
    
    this.dividerState.update(state => ({
      ...state,
      vertical: [...state.vertical, divider],
      canConfirm: true
    }));
  }
  
  /**
   * Update divider position
   */
  onDividerPositionChanged(event: { id: string; position: number }): void {
    const canvasSize = this.getCanvasSize();
    
    this.dividerState.update(state => {
      const horizontal = state.horizontal.map(d =>
        d.id === event.id 
          ? this.dividerManager.updateDividerPosition(d, event.position, canvasSize)
          : d
      );
      
      const vertical = state.vertical.map(d =>
        d.id === event.id 
          ? this.dividerManager.updateDividerPosition(d, event.position, canvasSize)
          : d
      );
      
      return { ...state, horizontal, vertical };
    });
  }
  
  /**
   * Remove divider
   */
  removeDivider(dividerId: string): void {
    this.dividerState.update(state => ({
      ...state,
      horizontal: state.horizontal.filter(d => d.id !== dividerId),
      vertical: state.vertical.filter(d => d.id !== dividerId),
      canConfirm: (state.horizontal.length + state.vertical.length) > 1
    }));
  }
  
  /**
   * Confirm dividers and generate sections
   */
  confirmDividers(): void {
    const state = this.dividerState();
    if (!state.canConfirm) return;
    
    const canvasSize = this.getCanvasSize();
    const layoutType = this.layoutConfig().type;
    
    // Generate sections from dividers
    const generatedSections = this.dividerManager.generateSectionsFromDividers(
      state,
      canvasSize,
      layoutType
    );
    
    // Convert to SectionConfig
    const sections: SectionConfig[] = generatedSections.map(gs => ({
      id: gs.id,
      name: gs.name,
      contentType: 'empty',
      flexbox: gs.flexbox,
      grid: gs.gridArea,
      positioning: { position: 'relative' },
      behavior: {
        scrollable: true,
        collapsible: false,
        resizable: true,
        draggable: true
      },
      visibility: {
        mobile: true,
        tablet: true,
        desktop: true
      },
      padding: '16px',
      margin: '0',
      borderRadius: '8px',
      createdAt: new Date(),
      updatedAt: new Date()
    }));
    
    // Update layout configuration
    const updatedConfig: LayoutConfiguration = {
      ...this.layoutConfig(),
      sections
    };
    
    this.layoutConfig.set(updatedConfig);
    this.saveLayoutConfiguration();
    this.layoutChanged.emit(updatedConfig);
    
    // Exit divide mode
    this.exitDivideMode();
    
    this.logger.info('Sections generated from dividers', {
      dividerCount: state.horizontal.length + state.vertical.length,
      sectionCount: sections.length
    });
  }
  
  /**
   * Cancel dividers
   */
  cancelDividers(): void {
    this.dividerState.set({
      horizontal: [],
      vertical: [],
      isActive: false,
      canConfirm: false
    });
    
    this.setCanvasMode('select');
  }
  
  /**
   * Exit divide mode
   */
  private exitDivideMode(): void {
    this.dividerState.set({
      horizontal: [],
      vertical: [],
      isActive: false,
      canConfirm: false
    });
    
    this.setCanvasMode('select');
  }
  
  /**
   * Get canvas size
   */
  private getCanvasSize(): { width: number; height: number } {
    if (!this.canvasContainer) {
      return { width: 1920, height: 1080 };
    }
    
    const el = this.canvasContainer.nativeElement;
    return {
      width: el.offsetWidth,
      height: el.offsetHeight
    };
  }
}

Template Updates

<!-- grid-canvas.component.html (additions) -->

<!-- Divider Mode Toolbar -->
<div class="divider-toolbar" *ngIf="canvasMode() === 'divide'">
  <button (click)="addHorizontalDivider()" class="toolbar-btn">
    <span class="icon">➡️</span>
    Add Horizontal Line
  </button>
  
  <button (click)="addVerticalDivider()" class="toolbar-btn">
    <span class="icon">⬇️</span>
    Add Vertical Line
  </button>
  
  <div class="divider-actions">
    <button 
      (click)="confirmDividers()" 
      [disabled]="!dividerState().canConfirm"
      class="btn-primary"
    >
      ✓ Confirm & Generate Sections
    </button>
    
    <button (click)="cancelDividers()" class="btn-secondary">
      ✗ Cancel
    </button>
  </div>
</div>

<!-- Divider Lines Overlay -->
<div class="dividers-container" *ngIf="dividerState().isActive">
  <lib-divider-tool
    *ngFor="let divider of activeDividers()"
    [divider]="divider"
    [canvasSize]="getCanvasSize()"
    (positionChanged)="onDividerPositionChanged($event)"
    (onRemove)="removeDivider($event)"
  />
</div>

<!-- Section Preview (during divide mode) -->
<div class="section-preview" *ngIf="dividerState().isActive">
  <div 
    *ngFor="let section of previewSections()"
    class="preview-section"
    [style.top.px]="section.bounds.top"
    [style.left.px]="section.bounds.left"
    [style.width.px]="section.bounds.width"
    [style.height.px]="section.bounds.height"
  >
    <span class="section-label">{{ section.name }}</span>
  </div>
</div>

💾 STORAGE ARCHITECTURE

Universe-Scoped Storage Schema

// Storage key pattern
`modular-universe/layout/${spaceId}/${universeId}/${moduleType}`

// Example keys
'modular-universe/layout/space-123/universe-456/universe'
'modular-universe/layout/space-123/universe-456/galaxy'

// Stored data
interface StoredLayoutConfiguration extends LayoutConfiguration {
  spaceId: string;
  universeId: string;
  moduleType: 'universe' | 'galaxy';
  version: string;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}

Section as Universal Module

// Each section becomes a module
const sectionModule: UniversalModule = {
  id: section.id,
  name: section.name,
  type: 'layout',  // or 'custom'
  universeId: universeId,
  parentId: universeId, // Parent is the universe/galaxy
  config: {
    ...section,
    // Section-specific config
  },
  isActive: true,
  createdAt: section.createdAt,
  updatedAt: section.updatedAt
};

// Save to storage
await storage.putInUniverse(spaceId, universeId, `module:${section.id}`, sectionModule);

// Register with ModuleRegistry (if reusable)
if (section.isReusable) {
  moduleRegistry.registerModule(sectionModule.manifest);
}

Flash Persistence Integration

// Auto-save every change
storage.put(key, data, {
  flashPersistence: true,  // Instant sync
  syncToBackend: true,      // Backend backup
  debounceMs: 500          // Debounce saves
});

// Cross-tab sync
storage.onChange((event) => {
  if (event.key.startsWith('modular-universe/layout/')) {
    // Another tab updated layout
    this.loadLayoutConfiguration();
  }
});

📋 TEMPLATE PRESETS

Common Layout Templates

// services/layout-templates.service.ts

export interface LayoutTemplate {
  id: string;
  name: string;
  description: string;
  preview: string;  // SVG or image URL
  layoutType: 'flexbox' | 'grid';
  dividers: {
    horizontal: number[];  // Positions
    vertical: number[];
  };
  sections: Partial<SectionConfig>[];
}

export const LAYOUT_TEMPLATES: LayoutTemplate[] = [
  {
    id: 'header-content-footer',
    name: 'Header / Content / Footer',
    description: 'Classic 3-section layout',
    preview: '/assets/templates/header-content-footer.svg',
    layoutType: 'flexbox',
    dividers: {
      horizontal: [80, -60],  // 80px from top, 60px from bottom
      vertical: []
    },
    sections: [
      { name: 'Header', contentType: 'module', flexbox: { flexGrow: 0, flexShrink: 0, flexBasis: '80px' } },
      { name: 'Content', contentType: 'empty', flexbox: { flexGrow: 1, flexShrink: 1, flexBasis: 'auto' } },
      { name: 'Footer', contentType: 'module', flexbox: { flexGrow: 0, flexShrink: 0, flexBasis: '60px' } }
    ]
  },
  
  {
    id: 'sidebar-content',
    name: 'Sidebar / Content',
    description: '2-column layout with sidebar',
    preview: '/assets/templates/sidebar-content.svg',
    layoutType: 'grid',
    dividers: {
      horizontal: [],
      vertical: [300]  // 300px from left
    },
    sections: [
      { name: 'Sidebar', contentType: 'module', grid: { row: 1, col: 1, rowSpan: 1, colSpan: 1 } },
      { name: 'Content', contentType: 'empty', grid: { row: 1, col: 2, rowSpan: 1, colSpan: 1 } }
    ]
  },
  
  {
    id: 'header-sidebar-content-footer',
    name: 'Full Dashboard Layout',
    description: 'Header + Sidebar + Content + Footer',
    preview: '/assets/templates/dashboard.svg',
    layoutType: 'grid',
    dividers: {
      horizontal: [80, -60],
      vertical: [250]
    },
    sections: [
      { name: 'Header', grid: { row: 1, col: 1, rowSpan: 1, colSpan: 2 } },
      { name: 'Sidebar', grid: { row: 2, col: 1, rowSpan: 1, colSpan: 1 } },
      { name: 'Content', grid: { row: 2, col: 2, rowSpan: 1, colSpan: 1 } },
      { name: 'Footer', grid: { row: 3, col: 1, rowSpan: 1, colSpan: 2 } }
    ]
  }
];

@Injectable()
export class LayoutTemplatesService {
  
  /**
   * Apply template to canvas
   */
  applyTemplate(
    template: LayoutTemplate,
    canvasSize: { width: number; height: number }
  ): LayoutConfiguration {
    // Convert template to layout configuration
    // ...
  }
}

🎯 IMPLEMENTATION ROADMAP

Week 1: Divider Foundation

Week 2: Cutting Mechanic

Week 3: Grid Integration

Week 4: Templates & Polish

Week 5: Storage & Module Integration


🎯 SUCCESS METRICS

Technical

User Experience

Platform Integration


📚 RELATED DOCUMENTS


Status: ✅ SPECIFICATION COMPLETE
Next Action: Begin Week 1 implementation
Owner: Development Team
Last Updated: November 14, 2025