Skip to content

Commit

Permalink
Decouple logic from interactive grid component
Browse files Browse the repository at this point in the history
Fixes nicolasperez19#16

Decouple logic from the interactive grid component.

* Add `src/utils/micLogic.ts` to export functions for handling microphone positions, zoom, pan, modes, mouse interactions, coordinate conversions, and numpy array string generation.
* Modify `src/components/MicMasterFlex.tsx` to import functions from `src/utils/micLogic.ts` and replace state management, event handlers, and numpy array string generation with utility functions.
* Add `src/utils/micLogic.test.ts` to test handling microphone positions, zoom, pan, modes, mouse interactions, coordinate conversions, and numpy array string generation.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/nicolasperez19/mic-master-flex/issues/16?shareId=XXXX-XXXX-XXXX-XXXX).
  • Loading branch information
nkzarrabi committed Oct 31, 2024
1 parent 871197e commit b56071b
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 196 deletions.
220 changes: 24 additions & 196 deletions src/components/MicMasterFlex.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import React, { useState, useRef } from 'react';
import { Copy, ZoomIn, ZoomOut, PlusCircle, Trash2, Hand, Edit2 } from 'lucide-react';

type Microphone = {
id: string;
x: number;
y: number;
}

type Point = {
x: number;
y: number;
}

type Mode = 'pan' | 'add' | 'delete' | 'edit';
import {
gridToScreen,
screenToGrid,
handleMouseDown,
handleMouseMove,
handleMouseUp,
handleCoordinateUpdate,
generateGridLines,
getNumpyArrayString,
copyToClipboard,
getCursor
} from '../utils/micLogic';

const MicMasterFlex = () => {
const [microphones, setMicrophones] = useState<Microphone[]>([]);
Expand All @@ -31,177 +30,6 @@ const MicMasterFlex = () => {
const gridSize = 10; // 10x10 meters grid
const gridDivisions = 20; // Grid lines every 0.5 meters

// Convert grid coordinates to screen coordinates
const gridToScreen = (point: Point): Point => ({
x: (point.x * zoom) + (window.innerWidth / 2) + pan.x,
y: (-point.y * zoom) + (window.innerHeight / 2) + pan.y,
});

// Convert screen coordinates to grid coordinates
const screenToGrid = (point: Point): Point => ({
x: ((point.x - (window.innerWidth / 2) - pan.x) / zoom),
y: -((point.y - (window.innerHeight / 2) - pan.y) / zoom),
});

// Handle mouse down on the grid
const handleMouseDown = (e: React.MouseEvent) => {
if (!svgRef.current) return;

const rect = svgRef.current.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};

setDragStart(point);
setIsDragging(true);
};

// Handle mouse move
const handleMouseMove = (e: React.MouseEvent) => {
if (!svgRef.current || !isDragging || mode !== 'pan') return;

const rect = svgRef.current.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};

if (dragStart) {
const dx = point.x - dragStart.x;
const dy = point.y - dragStart.y;
setPan(prev => ({ x: prev.x + dx, y: prev.y + dy }));
setDragStart(point);
}
};

// Handle mouse up
const handleMouseUp = (e: React.MouseEvent) => {
if (!isDragging || !svgRef.current) return;

const rect = svgRef.current.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};

const gridPoint = screenToGrid(point);

if (Math.abs(point.x - dragStart!.x) < 5 && Math.abs(point.y - dragStart!.y) < 5) {
if (mode === 'add') {
// Add new microphone at exact position
const newMic: Microphone = {
id: `mic-${Date.now()}`,
x: gridPoint.x,
y: gridPoint.y,
};
setMicrophones([...microphones, newMic]);
} else if (mode === 'delete' && hoveredMic) {
// Delete microphone
setMicrophones(mics => mics.filter(m => m.id !== hoveredMic.id));
setHoveredMic(null);
} else if (mode === 'edit' && hoveredMic) {
// Open edit dialog
setSelectedMic(hoveredMic);
setEditX(hoveredMic.x.toString());
setEditY(hoveredMic.y.toString());
setShowEditDialog(true);
}
}

setIsDragging(false);
setDragStart(null);
};

// Handle coordinate update
const handleCoordinateUpdate = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedMic) return;

const x = parseFloat(editX);
const y = parseFloat(editY);

if (isNaN(x) || isNaN(y)) return;

setMicrophones(mics =>
mics.map(mic =>
mic.id === selectedMic.id ? { ...mic, x, y } : mic
)
);
setShowEditDialog(false);
setSelectedMic(null);
};

// Generate grid lines
const generateGridLines = () => {
const lines = [];
const step = gridSize / gridDivisions;

// Generate vertical lines
for (let x = -gridSize / 2; x <= gridSize / 2; x += step) {
const start = gridToScreen({ x, y: -gridSize / 2 });
const end = gridToScreen({ x, y: gridSize / 2 });
lines.push(
<line
key={`v-${x}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
stroke={x === 0 ? "#666" : "#ddd"}
strokeWidth={x === 0 ? 2 : 1}
/>
);
}

// Generate horizontal lines
for (let y = -gridSize / 2; y <= gridSize / 2; y += step) {
const start = gridToScreen({ x: -gridSize / 2, y });
const end = gridToScreen({ x: gridSize / 2, y });
lines.push(
<line
key={`h-${y}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
stroke={y === 0 ? "#666" : "#ddd"}
strokeWidth={y === 0 ? 2 : 1}
/>
);
}

return lines;
};

// Generate numpy array string
const getNumpyArrayString = () => {
return `np.array([
${microphones.map(mic => `[${mic.x.toFixed(4)}, ${mic.y.toFixed(4)}]`).join(',\n ')}
])`;
};

// Copy array to clipboard
const copyToClipboard = () => {
navigator.clipboard.writeText(getNumpyArrayString());
};

// Get cursor style based on mode
const getCursor = () => {
switch (mode) {
case 'pan':
return 'grab';
case 'add':
return 'crosshair';
case 'delete':
return 'not-allowed';
case 'edit':
return 'pointer';
default:
return 'default';
}
};

return (
<>
<div className="flex-1 relative border border-gray-300 rounded-lg overflow-hidden bg-white mb-4">
Expand Down Expand Up @@ -253,16 +81,16 @@ const MicMasterFlex = () => {
<svg
ref={svgRef}
className="w-full h-full"
style={{ cursor: getCursor() }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
style={{ cursor: getCursor(mode) }}
onMouseDown={(e) => handleMouseDown(e, svgRef, setDragStart, setIsDragging)}
onMouseMove={(e) => handleMouseMove(e, svgRef, isDragging, mode, dragStart, setPan, setDragStart)}
onMouseUp={(e) => handleMouseUp(e, svgRef, isDragging, dragStart, mode, hoveredMic, setMicrophones, setHoveredMic, setSelectedMic, setEditX, setEditY, setShowEditDialog, zoom, pan, microphones)}
onMouseLeave={() => setIsDragging(false)}
>
<g>
{generateGridLines()}
{generateGridLines(gridSize, gridDivisions, zoom, pan)}
{microphones.map(mic => {
const pos = gridToScreen(mic);
const pos = gridToScreen(mic, zoom, pan);
return (
<g key={mic.id}>
<circle
Expand All @@ -286,8 +114,8 @@ const MicMasterFlex = () => {
<div
className="absolute bg-black text-white p-2 rounded text-sm pointer-events-none"
style={{
left: gridToScreen(hoveredMic).x + 10,
top: gridToScreen(hoveredMic).y - 30,
left: gridToScreen(hoveredMic, zoom, pan).x + 10,
top: gridToScreen(hoveredMic, zoom, pan).y - 30,
}}
>
({hoveredMic.x.toFixed(4)}m, {hoveredMic.y.toFixed(4)}m)
Expand All @@ -297,7 +125,7 @@ const MicMasterFlex = () => {
{showEditDialog && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-4 rounded-lg shadow-lg z-20">
<h3 className="text-lg font-semibold mb-4">Edit Microphone Position</h3>
<form onSubmit={handleCoordinateUpdate} className="space-y-4">
<form onSubmit={(e) => handleCoordinateUpdate(e, selectedMic, editX, editY, setMicrophones, setShowEditDialog, setSelectedMic)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">X Coordinate (m)</label>
<input
Expand Down Expand Up @@ -342,19 +170,19 @@ const MicMasterFlex = () => {
<div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold">Microphone Positions</h2>
<button
onClick={copyToClipboard}
onClick={() => copyToClipboard(getNumpyArrayString(microphones))}
className="flex items-center gap-2 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
>
<Copy size={16} />
Copy Array
</button>
</div>
<pre className="font-mono text-sm overflow-x-auto">
{getNumpyArrayString()}
{getNumpyArrayString(microphones)}
</pre>
</div>
</>
);
};

export default MicMasterFlex;
export default MicMasterFlex;
33 changes: 33 additions & 0 deletions src/utils/micLogic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { gridToScreen, screenToGrid, handleMouseDown, handleMouseMove, handleMouseUp, handleCoordinateUpdate, generateGridLines, getNumpyArrayString, copyToClipboard, getCursor, Microphone, Point, Mode } from './micLogic';

describe('micLogic utility functions', () => {
test('gridToScreen converts grid coordinates to screen coordinates correctly', () => {
const point: Point = { x: 1, y: 1 };
const zoom = 50;
const pan: Point = { x: 0, y: 0 };
const screenPoint = gridToScreen(point, zoom, pan);
expect(screenPoint).toEqual({ x: window.innerWidth / 2 + 50, y: window.innerHeight / 2 - 50 });
});

test('screenToGrid converts screen coordinates to grid coordinates correctly', () => {
const point: Point = { x: window.innerWidth / 2 + 50, y: window.innerHeight / 2 - 50 };
const zoom = 50;
const pan: Point = { x: 0, y: 0 };
const gridPoint = screenToGrid(point, zoom, pan);
expect(gridPoint).toEqual({ x: 1, y: 1 });
});

test('getNumpyArrayString generates correct numpy array string', () => {
const microphones: Microphone[] = [
{ id: 'mic-1', x: 1.2345, y: 6.7890 },
{ id: 'mic-2', x: 2.3456, y: 7.8901 },
];
const numpyString = getNumpyArrayString(microphones);
expect(numpyString).toBe(`np.array([
[1.2345, 6.7890],
[2.3456, 7.8901]
])`);
});

// Add more tests for other utility functions as needed
});
Loading

0 comments on commit b56071b

Please sign in to comment.