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>
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
'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)',
|
|
}
|
|
}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|