Skip to content

Background Grid & Guides System (Snap-to-Grid, Rulers, Alignment) #48

@bchou9

Description

@bchou9

Feature Description

Implement professional grid system, rulers, and alignment guides to help users create precise, well-aligned drawings and diagrams.

Problem Statement

Current limitations for precision work:

  • No visual grid reference
  • Difficult to align objects
  • No measurement tools
  • Freehand drawing only (imprecise)
  • No snap-to-grid functionality
  • Hard to create straight lines without guides

Proposed Features

1. Customizable Grid System

// frontend/src/components/GridOverlay.jsx
export function GridOverlay({ canvas, settings }) {
  const {
    enabled,
    gridSize,
    gridColor,
    gridOpacity,
    gridType, // 'square', 'dots', 'isometric'
    showSubGrid,
    subGridDivisions
  } = settings;

  useEffect(() => {
    
    const ctx = canvas.getContext('2d');
    drawGrid(ctx);
  }, [enabled, gridSize, gridType]);

  const drawGrid = (ctx) => {
    ctx.save();
    ctx.strokeStyle = gridColor;
    ctx.globalAlpha = gridOpacity;
    ctx.lineWidth = 0.5;

    const width = canvas.width;
    const height = canvas.height;

    switch (gridType) {
      case 'square':
        drawSquareGrid(ctx, width, height, gridSize);
        break;
      case 'dots':
        drawDotGrid(ctx, width, height, gridSize);
        break;
      case 'isometric':
        drawIsometricGrid(ctx, width, height, gridSize);
        break;
    }

    ctx.restore();
  };

  const drawSquareGrid = (ctx, width, height, size) => {
    // Vertical lines
    for (let x = 0; x <= width; x += size) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, height);
      ctx.stroke();
    }

    // Horizontal lines
    for (let y = 0; y <= height; y += size) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(width, y);
      ctx.stroke();
    }

    // Sub-grid (lighter)
    if (showSubGrid) {
      ctx.globalAlpha = gridOpacity * 0.3;
      const subSize = size / subGridDivisions;
      
      for (let x = 0; x <= width; x += subSize) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, height);
        ctx.stroke();
      }
      
      for (let y = 0; y <= height; y += subSize) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(width, y);
        ctx.stroke();
      }
    }
  };

  const drawDotGrid = (ctx, width, height, size) => {
    ctx.fillStyle = gridColor;
    const dotRadius = 1.5;

    for (let x = 0; x <= width; x += size) {
      for (let y = 0; y <= height; y += size) {
        ctx.beginPath();
        ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  };

  const drawIsometricGrid = (ctx, width, height, size) => {
    const angle = Math.PI / 6; // 30 degrees
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);

    // Draw diagonal lines
    for (let i = -height; i < width + height; i += size) {
      // Left-to-right diagonals
      ctx.beginPath();
      ctx.moveTo(i, 0);
      ctx.lineTo(i + height * cos, height);
      ctx.stroke();

      // Right-to-left diagonals
      ctx.beginPath();
      ctx.moveTo(i, 0);
      ctx.lineTo(i - height * cos, height);
      ctx.stroke();
    }

    // Horizontal lines
    for (let y = 0; y <= height; y += size * sin) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(width, y);
      ctx.stroke();
    }
  };

  return null; // Rendered directly to canvas
}

2. Snap-to-Grid System

// frontend/src/utils/SnapToGrid.js
export class SnapToGrid {
  constructor(gridSize, enabled = true) {
    this.gridSize = gridSize;
    this.enabled = enabled;
    this.snapThreshold = 10; // pixels
  }

  snap(point) {

    return {
      x: Math.round(point.x / this.gridSize) * this.gridSize,
      y: Math.round(point.y / this.gridSize) * this.gridSize
    };
  }

  snapIfClose(point) {
    const snapped = this.snap(point);
    const distance = Math.sqrt(
      Math.pow(snapped.x - point.x, 2) + 
      Math.pow(snapped.y - point.y, 2)
    );

    return distance <= this.snapThreshold ? snapped : point;
  }

  enable() {
    this.enabled = true;
  }

  disable() {
    this.enabled = false;
  }

  toggle() {
  }

  setGridSize(size) {
    this.gridSize = size;
  }
}

3. Ruler System

// frontend/src/components/Rulers.jsx
export function Rulers({ canvas, zoom, offset }) {
  const RULER_SIZE = 20;

  const drawHorizontalRuler = (ctx) => {
    ctx.fillStyle = '#f0f0f0';
    ctx.fillRect(0, 0, canvas.width, RULER_SIZE);
    
    ctx.strokeStyle = '#333';
    ctx.fillStyle = '#333';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';

    const step = 50 * zoom;
    const offset_x = offset.x % step;

    for (let x = -offset_x; x < canvas.width; x += step) {
      const realX = (x - offset.x) / zoom;
      
      // Major tick
      ctx.beginPath();
      ctx.moveTo(x, RULER_SIZE);
      ctx.lineTo(x, RULER_SIZE - 10);
      ctx.stroke();

      // Label
      ctx.fillText(Math.round(realX), x, RULER_SIZE - 12);

      // Minor ticks
      for (let i = 1; i < 5; i++) {
        const minorX = x + (step / 5) * i;
        if (minorX < canvas.width) {
          ctx.beginPath();
          ctx.moveTo(minorX, RULER_SIZE);
          ctx.lineTo(minorX, RULER_SIZE - 5);
          ctx.stroke();
        }
      }
    }
  };

  const drawVerticalRuler = (ctx) => {
    ctx.fillStyle = '#f0f0f0';
    ctx.fillRect(0, 0, RULER_SIZE, canvas.height);
    
    ctx.strokeStyle = '#333';
    ctx.fillStyle = '#333';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'right';

    const step = 50 * zoom;
    const offset_y = offset.y % step;

    for (let y = -offset_y; y < canvas.height; y += step) {
      const realY = (y - offset.y) / zoom;
      
      // Major tick
      ctx.beginPath();
      ctx.moveTo(RULER_SIZE, y);
      ctx.lineTo(RULER_SIZE - 10, y);
      ctx.stroke();

      // Label
      ctx.save();
      ctx.translate(RULER_SIZE - 2, y);
      ctx.rotate(-Math.PI / 2);
      ctx.fillText(Math.round(realY), 0, 0);
      ctx.restore();

      // Minor ticks
      for (let i = 1; i < 5; i++) {
        const minorY = y + (step / 5) * i;
        if (minorY < canvas.height) {
          ctx.beginPath();
          ctx.moveTo(RULER_SIZE, minorY);
          ctx.lineTo(RULER_SIZE - 5, minorY);
          ctx.stroke();
        }
      }
    }
  };

  return (
    <>
      <canvas 
        id="horizontal-ruler" 
        width={canvas.width} 
        height={RULER_SIZE}
        style={{ position: 'absolute', top: 0, left: RULER_SIZE }}
      />
      <canvas 
        id="vertical-ruler" 
        width={RULER_SIZE} 
        height={canvas.height}
        style={{ position: 'absolute', top: RULER_SIZE, left: 0 }}
      />
    </>
  );
}

4. Smart Alignment Guides

// frontend/src/utils/AlignmentGuides.js
export class AlignmentGuides {
  constructor(canvas) {
    this.canvas = canvas;
    this.guides = [];
    this.snapDistance = 5;
  }

  detectAlignments(movingObject, otherObjects) {
    this.guides = [];
    
    const movingBounds = this.getBounds(movingObject);

    for (const obj of otherObjects) {
      if (obj.id === movingObject.id) continue;
      
      const bounds = this.getBounds(obj);

      // Check horizontal alignments
      if (Math.abs(movingBounds.centerY - bounds.centerY) < this.snapDistance) {
        this.guides.push({
          type: 'horizontal',
          y: bounds.centerY,
          x1: Math.min(movingBounds.left, bounds.left),
          x2: Math.max(movingBounds.right, bounds.right)
        });
      }

      if (Math.abs(movingBounds.top - bounds.top) < this.snapDistance) {
        this.guides.push({
          type: 'horizontal',
          y: bounds.top,
          x1: Math.min(movingBounds.left, bounds.left),
          x2: Math.max(movingBounds.right, bounds.right)
        });
      }

      if (Math.abs(movingBounds.bottom - bounds.bottom) < this.snapDistance) {
        this.guides.push({
          type: 'horizontal',
          y: bounds.bottom,
          x1: Math.min(movingBounds.left, bounds.left),
          x2: Math.max(movingBounds.right, bounds.right)
        });
      }

      // Check vertical alignments
      if (Math.abs(movingBounds.centerX - bounds.centerX) < this.snapDistance) {
        this.guides.push({
          type: 'vertical',
          x: bounds.centerX,
          y1: Math.min(movingBounds.top, bounds.top),
          y2: Math.max(movingBounds.bottom, bounds.bottom)
        });
      }

      if (Math.abs(movingBounds.left - bounds.left) < this.snapDistance) {
        this.guides.push({
          type: 'vertical',
          x: bounds.left,
          y1: Math.min(movingBounds.top, bounds.top),
          y2: Math.max(movingBounds.bottom, bounds.bottom)
        });
      }

      if (Math.abs(movingBounds.right - bounds.right) < this.snapDistance) {
        this.guides.push({
          type: 'vertical',
          x: bounds.right,
          y1: Math.min(movingBounds.top, bounds.top),
          y2: Math.max(movingBounds.bottom, bounds.bottom)
        });
      }
    }

    return this.guides;
  }

  drawGuides(ctx) {
    ctx.save();
    ctx.strokeStyle = '#ff00ff';
    ctx.lineWidth = 1;
    ctx.setLineDash([5, 5]);

    for (const guide of this.guides) {
      ctx.beginPath();
      if (guide.type === 'horizontal') {
        ctx.moveTo(guide.x1, guide.y);
        ctx.lineTo(guide.x2, guide.y);
      } else {
        ctx.moveTo(guide.x, guide.y1);
        ctx.lineTo(guide.x, guide.y2);
      }
      ctx.stroke();
    }

    ctx.restore();
  }

  getBounds(obj) {
    // Calculate bounding box for object
    // Implementation depends on object type
    return {
      left: obj.x,
      right: obj.x + obj.width,
      top: obj.y,
      bottom: obj.y + obj.height,
      centerX: obj.x + obj.width / 2,
      centerY: obj.y + obj.height / 2
    };
  }
}

5. Grid Settings Panel

// frontend/src/components/GridSettings.jsx
export function GridSettings({ settings, onChange }) {
  return (
    <Box p={2}>
      <Typography variant="h6" gutterBottom>Grid Settings</Typography>
      
      <FormControlLabel
        control={
          <Switch 
            checked={settings.enabled} 
            onChange={(e) => onChange({ ...settings, enabled: e.target.checked })}
          />
        }
        label="Show Grid"
      />

      <FormControl fullWidth margin="normal">
        <InputLabel>Grid Type</InputLabel>
        <Select 
          value={settings.gridType} 
          onChange={(e) => onChange({ ...settings, gridType: e.target.value })}
        >
          <MenuItem value="square">Square</MenuItem>
          <MenuItem value="dots">Dots</MenuItem>
          <MenuItem value="isometric">Isometric</MenuItem>
        </Select>
      </FormControl>

      <Typography gutterBottom>Grid Size: {settings.gridSize}px</Typography>
      <Slider
        value={settings.gridSize}
        min={10}
        max={100}
        step={5}
        onChange={(e, value) => onChange({ ...settings, gridSize: value })}
        marks
        valueLabelDisplay="auto"
      />

      <Typography gutterBottom>Opacity: {settings.gridOpacity}</Typography>
      <Slider
        value={settings.gridOpacity}
        min={0}
        max={1}
        step={0.1}
        onChange={(e, value) => onChange({ ...settings, gridOpacity: value })}
        valueLabelDisplay="auto"
      />

      <Box mt={2}>
        <input
          type="color"
          value={settings.gridColor}
          onChange={(e) => onChange({ ...settings, gridColor: e.target.value })}
        />
        <Typography variant="caption" ml={1}>Grid Color</Typography>
      </Box>

      <FormControlLabel
        control={
          <Switch 
            checked={settings.snapToGrid} 
            onChange={(e) => onChange({ ...settings, snapToGrid: e.target.checked })}
          />
        }
        label="Snap to Grid"
      />

      <FormControlLabel
        control={
          <Switch 
            checked={settings.showRulers} 
            onChange={(e) => onChange({ ...settings, showRulers: e.target.checked })}
          />
        }
        label="Show Rulers"
      />

      <FormControlLabel
        control={
          <Switch 
            checked={settings.showGuides} 
            onChange={(e) => onChange({ ...settings, showGuides: e.target.checked })}
          />
        }
        label="Smart Alignment Guides"
      />
    </Box>
  );
}

Keyboard Shortcuts

  • Ctrl/Cmd + ' - Toggle grid visibility
  • Ctrl/Cmd + ; - Toggle snap to grid
  • Ctrl/Cmd + R - Toggle rulers
  • Ctrl/Cmd + G - Open grid settings

Files to Create/Modify

Frontend:

  • frontend/src/components/GridOverlay.jsx ⭐ (NEW)
  • frontend/src/components/Rulers.jsx ⭐ (NEW)
  • frontend/src/components/GridSettings.jsx ⭐ (NEW)
  • frontend/src/utils/SnapToGrid.js ⭐ (NEW)
  • frontend/src/utils/AlignmentGuides.js ⭐ (NEW)
  • frontend/src/components/Canvas.js (MODIFY)

Benefits

  • Professional precision tools
  • Easier to create aligned designs
  • Better for technical diagrams
  • Improved user experience
  • Competitive with tools like Figma, Sketch
  • Accessible measurement system

Use Cases

  • Architecture diagrams
  • UI/UX wireframes
  • Engineering schematics
  • Graph paper equivalents
  • Educational worksheets
  • Game level design

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions