feat(phase-13): implement rulers and guides system for precise alignment

Add comprehensive rulers and guides system for canvas alignment and positioning.

Features:
- Guides store with full CRUD operations
- Horizontal and vertical guide support
- Ruler display with 50px intervals
- Green guide lines with subtle glow
- Toggle visibility for rulers and guides
- Persistent storage of guides and settings
- Snap distance configuration
- Guide position in canvas pixels
- Zoom-aware positioning

Changes:
- Created store/guides-store.ts with Guide interface
- Added guides state management with persistence
- Created components/canvas/rulers-and-guides.tsx
- Rulers show measurements at 50px intervals
- Guides rendered as 1px green lines with shadow
- Exported guides store from store/index.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 19:59:45 +01:00
parent 4ddb8fe0e3
commit 19baa730c7
3 changed files with 222 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
'use client';
import { useGuidesStore } from '@/store';
import { useCanvasStore } from '@/store';
interface RulersAndGuidesProps {
canvasWidth: number;
canvasHeight: number;
}
export function RulersAndGuides({ canvasWidth, canvasHeight }: RulersAndGuidesProps) {
const { guides, showGuides, showRulers } = useGuidesStore();
const { zoom, offsetX, offsetY } = useCanvasStore();
if (!showGuides && !showRulers) return null;
const rulerSize = 20;
return (
<>
{/* Horizontal Ruler */}
{showRulers && (
<div
className="absolute top-0 left-0 bg-card border-b border-border"
style={{
width: `${canvasWidth}px`,
height: `${rulerSize}px`,
marginLeft: `${rulerSize}px`,
}}
>
<svg width={canvasWidth} height={rulerSize}>
{Array.from({ length: Math.ceil(canvasWidth / (zoom * 50)) }).map((_, i) => {
const x = i * 50 * zoom + offsetX;
if (x < 0 || x > canvasWidth) return null;
return (
<g key={i}>
<line
x1={x}
y1={rulerSize - 5}
x2={x}
y2={rulerSize}
stroke="currentColor"
strokeWidth="1"
className="text-muted-foreground"
/>
<text
x={x + 2}
y={rulerSize - 8}
fontSize="10"
className="text-muted-foreground fill-current"
>
{i * 50}
</text>
</g>
);
})}
</svg>
</div>
)}
{/* Vertical Ruler */}
{showRulers && (
<div
className="absolute top-0 left-0 bg-card border-r border-border"
style={{
width: `${rulerSize}px`,
height: `${canvasHeight}px`,
marginTop: `${rulerSize}px`,
}}
>
<svg width={rulerSize} height={canvasHeight}>
{Array.from({ length: Math.ceil(canvasHeight / (zoom * 50)) }).map((_, i) => {
const y = i * 50 * zoom + offsetY;
if (y < 0 || y > canvasHeight) return null;
return (
<g key={i}>
<line
x1={rulerSize - 5}
y1={y}
x2={rulerSize}
y2={y}
stroke="currentColor"
strokeWidth="1"
className="text-muted-foreground"
/>
<text
x={2}
y={y - 2}
fontSize="10"
className="text-muted-foreground fill-current"
transform={`rotate(-90 ${2} ${y - 2})`}
>
{i * 50}
</text>
</g>
);
})}
</svg>
</div>
)}
{/* Guides */}
{showGuides && guides.map((guide) => (
<div
key={guide.id}
className="absolute pointer-events-none"
style={
guide.type === 'horizontal'
? {
top: `${guide.position * zoom + offsetY}px`,
left: 0,
right: 0,
height: '1px',
backgroundColor: '#00ff00',
boxShadow: '0 0 2px rgba(0, 255, 0, 0.5)',
}
: {
left: `${guide.position * zoom + offsetX}px`,
top: 0,
bottom: 0,
width: '1px',
backgroundColor: '#00ff00',
boxShadow: '0 0 2px rgba(0, 255, 0, 0.5)',
}
}
/>
))}
</>
);
}