384 lines
65beb53 samthecodingguy Jan 29, 2026
'use client';
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { useEditorStore } from '@/lib/store/editor-store';
import { v4 as uuidv4 } from 'uuid';
export interface CustomGuide {
  id: string;
  type: 'horizontal' | 'vertical';
  position: number; // Position in canvas coordinates
}
interface RulersProps {
  containerWidth: number;
  containerHeight: number;
  customGuides: CustomGuide[];
  onAddGuide: (guide: CustomGuide) => void;
  onUpdateGuide: (id: string, position: number) => void;
  onDeleteGuide: (id: string) => void;
}
const RULER_SIZE = 20;
const MAJOR_TICK_INTERVAL = 100;
const MINOR_TICK_INTERVAL = 10;
export function Rulers({
  containerWidth,
  containerHeight,
  customGuides,
  onAddGuide,
  onUpdateGuide,
  onDeleteGuide
}: RulersProps) {
  const horizontalCanvasRef = useRef<HTMLCanvasElement>(null);
  const verticalCanvasRef = useRef<HTMLCanvasElement>(null);
  const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null);
  const [isDraggingGuide, setIsDraggingGuide] = useState<{
    type: 'horizontal' | 'vertical';
    id: string | null; // null means creating new guide
    startPos: number;
  } | null>(null);
  const { canvas, showGuides } = useEditorStore();
  // Don't render if guides are disabled
  if (!showGuides) return null;
  // Draw horizontal ruler
  useEffect(() => {
    const ctx = horizontalCanvasRef.current?.getContext('2d');
    if (!ctx) return;
    const dpr = window.devicePixelRatio || 1;
    const canvasEl = horizontalCanvasRef.current!;
    canvasEl.width = containerWidth * dpr;
    canvasEl.height = RULER_SIZE * dpr;
    ctx.scale(dpr, dpr);
    // Clear
    ctx.fillStyle = '#1a1a1a';
    ctx.fillRect(0, 0, containerWidth, RULER_SIZE);
    // Draw ticks
    ctx.fillStyle = '#666';
    ctx.font = '9px Inter, system-ui, sans-serif';
    ctx.textAlign = 'center';
    const startX = -canvas.pan.x / canvas.zoom;
    const endX = startX + containerWidth / canvas.zoom;
    // Round to nearest interval
    const firstTick = Math.floor(startX / MINOR_TICK_INTERVAL) * MINOR_TICK_INTERVAL;
    for (let x = firstTick; x <= endX; x += MINOR_TICK_INTERVAL) {
      const screenX = (x * canvas.zoom) + canvas.pan.x;
      if (screenX < 0 || screenX > containerWidth) continue;
      const isMajor = x % MAJOR_TICK_INTERVAL === 0;
      const tickHeight = isMajor ? 10 : 5;
      ctx.fillStyle = isMajor ? '#888' : '#444';
      ctx.fillRect(screenX, RULER_SIZE - tickHeight, 1, tickHeight);
      if (isMajor && x >= 0) {
        ctx.fillStyle = '#888';
        ctx.fillText(String(x), screenX, 10);
      }
    }
    // Draw mouse position indicator
    if (mousePos) {
      ctx.fillStyle = '#f0abfc';
      ctx.fillRect(mousePos.x, 0, 1, RULER_SIZE);
    }
    // Border
    ctx.strokeStyle = '#333';
    ctx.strokeRect(0, 0, containerWidth, RULER_SIZE);
  }, [containerWidth, canvas.zoom, canvas.pan.x, mousePos]);
  // Draw vertical ruler
  useEffect(() => {
    const ctx = verticalCanvasRef.current?.getContext('2d');
    if (!ctx) return;
    const dpr = window.devicePixelRatio || 1;
    const canvasEl = verticalCanvasRef.current!;
    canvasEl.width = RULER_SIZE * dpr;
    canvasEl.height = containerHeight * dpr;
    ctx.scale(dpr, dpr);
    // Clear
    ctx.fillStyle = '#1a1a1a';
    ctx.fillRect(0, 0, RULER_SIZE, containerHeight);
    // Draw ticks
    ctx.fillStyle = '#666';
    ctx.font = '9px Inter, system-ui, sans-serif';
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    const startY = -canvas.pan.y / canvas.zoom;
    const endY = startY + containerHeight / canvas.zoom;
    const firstTick = Math.floor(startY / MINOR_TICK_INTERVAL) * MINOR_TICK_INTERVAL;
    for (let y = firstTick; y <= endY; y += MINOR_TICK_INTERVAL) {
      const screenY = (y * canvas.zoom) + canvas.pan.y;
      if (screenY < 0 || screenY > containerHeight) continue;
      const isMajor = y % MAJOR_TICK_INTERVAL === 0;
      const tickWidth = isMajor ? 10 : 5;
      ctx.fillStyle = isMajor ? '#888' : '#444';
      ctx.fillRect(RULER_SIZE - tickWidth, screenY, tickWidth, 1);
      if (isMajor && y >= 0) {
        ctx.save();
        ctx.translate(10, screenY);
        ctx.rotate(-Math.PI / 2);
        ctx.fillStyle = '#888';
        ctx.textAlign = 'center';
        ctx.fillText(String(y), 0, 0);
        ctx.restore();
      }
    }
    // Draw mouse position indicator
    if (mousePos) {
      ctx.fillStyle = '#f0abfc';
      ctx.fillRect(0, mousePos.y, RULER_SIZE, 1);
    }
    // Border
    ctx.strokeStyle = '#333';
    ctx.strokeRect(0, 0, RULER_SIZE, containerHeight);
  }, [containerHeight, canvas.zoom, canvas.pan.y, mousePos]);
  // Track mouse position
  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    setMousePos({
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
    });
  };
  const handleMouseLeave = () => {
    setMousePos(null);
  };
  // Convert screen position to canvas position
  const screenToCanvas = useCallback((screenX: number, screenY: number) => {
    return {
      x: (screenX - canvas.pan.x) / canvas.zoom,
      y: (screenY - canvas.pan.y) / canvas.zoom,
    };
  }, [canvas.pan.x, canvas.pan.y, canvas.zoom]);
  // Convert canvas position to screen position
  const canvasToScreen = useCallback((canvasX: number, canvasY: number) => {
    return {
      x: canvasX * canvas.zoom + canvas.pan.x,
      y: canvasY * canvas.zoom + canvas.pan.y,
    };
  }, [canvas.pan.x, canvas.pan.y, canvas.zoom]);
  // Handle ruler mouse down (start creating guide)
  const handleHorizontalRulerMouseDown = (e: React.MouseEvent) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const screenX = e.clientX - rect.left;
    const canvasPos = screenToCanvas(screenX, 0);
    setIsDraggingGuide({
      type: 'vertical',
      id: null,
      startPos: canvasPos.x,
    });
  };
  const handleVerticalRulerMouseDown = (e: React.MouseEvent) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const screenY = e.clientY - rect.top;
    const canvasPos = screenToCanvas(0, screenY);
    setIsDraggingGuide({
      type: 'horizontal',
      id: null,
      startPos: canvasPos.y,
    });
  };
  // Handle global mouse move/up for guide dragging
  useEffect(() => {
    if (!isDraggingGuide) return;
    const handleGlobalMouseMove = (e: MouseEvent) => {
      // Update guide position while dragging
      // This is handled by the parent component via the dragging line
    };
    const handleGlobalMouseUp = (e: MouseEvent) => {
      if (!isDraggingGuide) return;
      // Get the final position
      const container = document.querySelector('.canvas-container');
      if (container) {
        const rect = container.getBoundingClientRect();
        const screenPos = isDraggingGuide.type === 'vertical'
          ? e.clientX - rect.left - RULER_SIZE
          : e.clientY - rect.top - RULER_SIZE;
        const canvasPos = isDraggingGuide.type === 'vertical'
          ? screenToCanvas(screenPos, 0).x
          : screenToCanvas(0, screenPos).y;
        // Only create guide if it's within canvas bounds
        if (canvasPos >= 0 && canvasPos <= (isDraggingGuide.type === 'vertical' ? canvas.width : canvas.height)) {
          const newGuide: CustomGuide = {
            id: uuidv4(),
            type: isDraggingGuide.type,
            position: canvasPos,
          };
          onAddGuide(newGuide);
        }
      }
      setIsDraggingGuide(null);
    };
    document.addEventListener('mousemove', handleGlobalMouseMove);
    document.addEventListener('mouseup', handleGlobalMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleGlobalMouseMove);
      document.removeEventListener('mouseup', handleGlobalMouseUp);
    };
  }, [isDraggingGuide, screenToCanvas, canvas.width, canvas.height, onAddGuide]);
  // Handle guide drag start (for existing guides)
  const handleGuideDragStart = (guide: CustomGuide, e: React.MouseEvent) => {
    e.stopPropagation();
    setIsDraggingGuide({
      type: guide.type,
      id: guide.id,
      startPos: guide.position,
    });
  };
  // Handle guide double-click to delete
  const handleGuideDoubleClick = (guide: CustomGuide, e: React.MouseEvent) => {
    e.stopPropagation();
    onDeleteGuide(guide.id);
  };
  return (
    <>
      {/* Corner square */}
      <div
        className="absolute top-0 left-0 bg-neutral-900 border-r border-b border-neutral-700 z-20"
        style={{ width: RULER_SIZE, height: RULER_SIZE }}
      />
      {/* Horizontal ruler (drag down to create vertical guide) */}
      <div
        className="absolute top-0 z-10 cursor-col-resize"
        style={{
          left: RULER_SIZE,
          width: containerWidth - RULER_SIZE,
          height: RULER_SIZE,
        }}
        onMouseDown={handleHorizontalRulerMouseDown}
      >
        <canvas
          ref={horizontalCanvasRef}
          className="pointer-events-none"
          style={{
            width: '100%',
            height: '100%',
          }}
        />
      </div>
      {/* Vertical ruler (drag right to create horizontal guide) */}
      <div
        className="absolute left-0 z-10 cursor-row-resize"
        style={{
          top: RULER_SIZE,
          width: RULER_SIZE,
          height: containerHeight - RULER_SIZE,
        }}
        onMouseDown={handleVerticalRulerMouseDown}
      >
        <canvas
          ref={verticalCanvasRef}
          className="pointer-events-none"
          style={{
            width: '100%',
            height: '100%',
          }}
        />
      </div>
      {/* Custom guides */}
      {customGuides.map((guide) => {
        const screenPos = guide.type === 'vertical'
          ? canvasToScreen(guide.position, 0).x + RULER_SIZE
          : canvasToScreen(0, guide.position).y + RULER_SIZE;
        return (
          <div
            key={guide.id}
            className={`absolute ${guide.type === 'vertical' ? 'cursor-col-resize' : 'cursor-row-resize'}`}
            style={
              guide.type === 'vertical'
                ? {
                    left: screenPos,
                    top: RULER_SIZE,
                    width: 1,
                    height: containerHeight - RULER_SIZE,
                    backgroundColor: '#f0abfc',
                    zIndex: 15,
                  }
                : {
                    left: RULER_SIZE,
                    top: screenPos,
                    width: containerWidth - RULER_SIZE,
                    height: 1,
                    backgroundColor: '#f0abfc',
                    zIndex: 15,
                  }
            }
            onMouseDown={(e) => handleGuideDragStart(guide, e)}
            onDoubleClick={(e) => handleGuideDoubleClick(guide, e)}
            title="Drag to move, double-click to delete"
          >
            {/* Wider hit area */}
            <div
              className="absolute"
              style={
                guide.type === 'vertical'
                  ? { left: -4, width: 9, height: '100%' }
                  : { top: -4, height: 9, width: '100%' }
              }
            />
          </div>
        );
      })}
      {/* Mouse tracking overlay (invisible) */}
      <div
        className="absolute pointer-events-none"
        style={{ left: RULER_SIZE, top: RULER_SIZE, right: 0, bottom: 0 }}
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
      />
    </>
  );
}
export const RULER_SIZE_EXPORT = RULER_SIZE;