-
Notifications
You must be signed in to change notification settings - Fork 16
Open
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed
Description
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 visibilityCtrl/Cmd + ;- Toggle snap to gridCtrl/Cmd + R- Toggle rulersCtrl/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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed