diff --git a/CLAUDE.md b/CLAUDE.md index 05162b7..25491f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -293,18 +293,24 @@ Enable verbose logging: - ✅ Compilation via arduino-cli to .hex files - ✅ Real AVR8 emulation with avr8js - ✅ Pin state tracking and component updates -- ✅ Visual components (Arduino Uno, LEDs, resistors, buttons) -- ✅ Wire rendering (visual only, not functional) +- ✅ Dynamic component system with 48+ wokwi-elements components +- ✅ Component picker modal with search and categories +- ✅ Component property dialog (single-click interaction) +- ✅ Component rotation (90° increments) +- ✅ Wire creation and rendering (orthogonal routing) +- ✅ Segment-based wire editing (drag segments perpendicular to orientation) +- ✅ Real-time wire preview with grid snapping (20px) +- ✅ Pin overlay system for wire connections **In Progress:** -- 🚧 Wire editing and creation UI -- 🚧 Functional wire connections (signal routing) +- 🚧 Functional wire connections (electrical signal routing) +- 🚧 Wire validation and error handling **Planned:** -- 📋 More components (sensors, displays, motors) - 📋 Serial monitor - 📋 Project persistence (SQLite) - 📋 Multi-board support (Mega, Nano, ESP32) +- 📋 Undo/redo functionality ## Additional Resources diff --git a/frontend/README.md b/frontend/README.md index d2e7761..122b4ec 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,73 +1,216 @@ -# React + TypeScript + Vite +# Arduino Emulator - Frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +React + TypeScript + Vite frontend for the Arduino emulator with visual simulator and code editor. -Currently, two official plugins are available: +## Features -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- **Monaco Code Editor** - Full VSCode-like Arduino code editing experience +- **Dynamic Component System** - 48+ wokwi-elements components with search and categories +- **Visual Simulator Canvas** - Interactive drag-and-drop circuit builder +- **Component Property Dialog** - Single-click component interaction (rotate, delete, view pins) +- **Segment-Based Wire Editing** - Drag wire segments perpendicular to orientation (like Wokwi) +- **Real AVR8 Emulation** - Actual ATmega328p emulation using avr8js +- **Pin Management** - Automatic pin mapping and state synchronization +- **Grid Snapping** - 20px grid alignment for clean circuit layouts -## React Compiler +## Tech Stack -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +- **React** 18 - UI framework +- **TypeScript** - Static typing +- **Vite** 5 - Build tool and dev server +- **Monaco Editor** - Code editor (VSCode engine) +- **Zustand** - State management +- **Axios** - HTTP client for backend API +- **avr8js** - AVR8 CPU emulator (local clone) +- **@wokwi/elements** - Electronic web components (local clone) -## Expanding the ESLint configuration +## Development -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +### Prerequisites +- Node.js 18+ +- Backend running at http://localhost:8001 +- Wokwi libraries built in `../wokwi-libs/` -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### Install Dependencies +```bash +npm install ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### Run Development Server +```bash +npm run dev ``` + +The app will be available at http://localhost:5173 + +### Build for Production +```bash +npm run build +``` + +Output will be in the `dist/` directory. + +### Lint +```bash +npm run lint +``` + +## Project Structure + +``` +frontend/ +├── src/ +│ ├── components/ +│ │ ├── components-wokwi/ # React wrappers for wokwi-elements +│ │ ├── editor/ # Monaco Editor components +│ │ │ ├── CodeEditor.tsx +│ │ │ └── EditorToolbar.tsx +│ │ └── simulator/ # Simulation canvas components +│ │ ├── SimulatorCanvas.tsx +│ │ ├── WireLayer.tsx +│ │ ├── WireRenderer.tsx +│ │ ├── PinOverlay.tsx +│ │ ├── ComponentPropertyDialog.tsx +│ │ ├── ComponentPickerModal.tsx +│ │ └── ComponentPalette.tsx +│ ├── simulation/ +│ │ ├── AVRSimulator.ts # AVR8 CPU wrapper +│ │ └── PinManager.ts # Pin mapping and callbacks +│ ├── store/ +│ │ ├── useEditorStore.ts # Code editor state +│ │ └── useSimulatorStore.ts # Simulation state +│ ├── services/ +│ │ ├── api.ts # Backend API client +│ │ └── ComponentRegistry.ts # Component metadata +│ ├── types/ # TypeScript definitions +│ ├── utils/ +│ │ ├── hexParser.ts # Intel HEX parser +│ │ ├── wirePathGenerator.ts # Wire SVG path generation +│ │ └── wireSegments.ts # Segment-based wire editing +│ ├── App.tsx # Main app component +│ └── main.tsx # Entry point +├── public/ # Static assets +├── vite.config.ts # Vite configuration +└── package.json +``` + +## Key Architecture Patterns + +### State Management (Zustand) +Two main stores: +- **useEditorStore** - Code content, theme, compilation state +- **useSimulatorStore** - Simulation running state, components, wires, compiled hex + +### Local Wokwi Libraries +Vite aliases point to local clones instead of npm packages: +```typescript +resolve: { + alias: { + 'avr8js': path.resolve(__dirname, '../wokwi-libs/avr8js/dist/esm'), + '@wokwi/elements': path.resolve(__dirname, '../wokwi-libs/wokwi-elements/dist/esm'), + }, +} +``` + +### AVR Simulation Loop +- Runs at ~60 FPS using `requestAnimationFrame` +- Executes ~267,000 CPU cycles per frame (16MHz / 60fps) +- Port listeners fire when GPIO registers change +- PinManager routes pin states to component callbacks + +### Component System +Components are Web Components from wokwi-elements: +1. React wrappers in `components-wokwi/` +2. Dynamic loading via ComponentRegistry +3. Pin info extracted from component metadata +4. State updates via refs and callbacks + +### Wire Editing System +Segment-based editing (like Wokwi): +- Wires consist of orthogonal segments (horizontal/vertical) +- Drag segments perpendicular to orientation: + - Horizontal segments: move up/down (ns-resize) + - Vertical segments: move left/right (ew-resize) +- Local preview state during drag (requestAnimationFrame) +- Store update only on mouse up with grid snapping (20px) + +### Performance Optimizations +- `requestAnimationFrame` for smooth wire dragging +- Local state for real-time previews +- Memoized path generation and segment computation +- Store updates batched at interaction completion + +## API Integration + +Backend endpoints (http://localhost:8001): +- `POST /api/compile` - Compile Arduino code to .hex +- `GET /api/compile/status/{task_id}` - Check compilation status +- `GET /api/compile/download/{filename}` - Download compiled .hex + +See [backend documentation](../backend/README.md) for API details. + +## Component Development + +### Adding a New Component Type + +1. Check if wokwi-elements has the component: + ```bash + ls ../wokwi-libs/wokwi-elements/src/ + ``` + +2. Create React wrapper in `src/components/components-wokwi/`: + ```typescript + import React, { useRef, useEffect } from 'react'; + + export const WokwiMyComponent: React.FC = ({ ... }) => { + const elementRef = useRef(null); + + useEffect(() => { + if (elementRef.current) { + elementRef.current.setAttribute('prop', value); + } + }, [value]); + + return ; + }; + ``` + +3. Add to ComponentRegistry metadata + +4. Use in SimulatorCanvas or make available in ComponentPalette + +## Troubleshooting + +### Monaco Editor Not Loading +- Check if `monaco-editor` is installed +- Verify Vite worker configuration in vite.config.ts + +### Components Not Rendering +- Ensure wokwi-elements is built: `cd ../wokwi-libs/wokwi-elements && npm run build` +- Check browser console for Web Component registration errors +- Verify Vite alias paths in vite.config.ts + +### Wire Editing Performance Issues +- Ensure `requestAnimationFrame` is being used +- Check that store updates only happen on mouse up, not during drag +- Verify no unnecessary re-renders with React DevTools + +### Pin Alignment Issues +- Pin coordinates from wokwi-elements are in CSS pixels +- Do NOT multiply by MM_TO_PX conversion factor +- Verify component position + pin offset calculation + +### Compilation Fails +- Check backend is running at http://localhost:8001 +- Verify arduino-cli is installed and `arduino:avr` core is available +- Check CORS configuration in backend + +## References + +- [Main Project README](../README.md) +- [Development Guide (CLAUDE.md)](../CLAUDE.md) +- [Architecture Documentation](../ARCHITECTURE.md) +- [Wokwi Integration](../WOKWI_LIBS.md) +- [Monaco Editor API](https://microsoft.github.io/monaco-editor/api/index.html) +- [Vite Documentation](https://vitejs.dev/) +- [Zustand Guide](https://docs.pmnd.rs/zustand/getting-started/introduction) diff --git a/frontend/src/components/simulator/WireRenderer.tsx b/frontend/src/components/simulator/WireRenderer.tsx index 0364dc2..53c46f0 100644 --- a/frontend/src/components/simulator/WireRenderer.tsx +++ b/frontend/src/components/simulator/WireRenderer.tsx @@ -40,6 +40,7 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) const [previewOrthoPoints, setPreviewOrthoPoints] = useState | null>(null); const svgRef = useRef(null); + const rafRef = useRef(null); // For requestAnimationFrame // Generate SVG path (memoized for performance) // Use preview points during drag, actual wire points otherwise @@ -56,9 +57,41 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) }, [wire, previewOrthoPoints]); // Compute segments (memoized) + // Use preview points during drag for accurate segment positions const segments = useMemo(() => { + if (previewOrthoPoints) { + // During drag, compute segments from preview points + const previewSegments: WireSegment[] = []; + for (let i = 0; i < previewOrthoPoints.length - 1; i++) { + const start = previewOrthoPoints[i]; + const end = previewOrthoPoints[i + 1]; + + if (start.x === end.x && start.y === end.y) continue; + + const orientation = start.y === end.y ? 'horizontal' : 'vertical'; + const length = + orientation === 'horizontal' + ? Math.abs(end.x - start.x) + : Math.abs(end.y - start.y); + + previewSegments.push({ + id: `${wire.id}-seg-${i}`, + startPoint: start, + endPoint: end, + orientation, + midPoint: { + x: (start.x + end.x) / 2, + y: (start.y + end.y) / 2, + }, + length, + startIndex: i, + endIndex: i + 1, + }); + } + return previewSegments; + } return computeSegments(wire); - }, [wire]); + }, [wire, previewOrthoPoints]); // Handle wire selection const handleWireClick = useCallback( @@ -73,36 +106,54 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (dragState) { - // Handle dragging - use local state for smooth updates - const svg = svgRef.current?.ownerSVGElement; - if (!svg) return; - - const svgRect = svg.getBoundingClientRect(); - const mouseX = e.clientX - svgRect.left; - const mouseY = e.clientY - svgRect.top; - - const { segment, startMousePos, originalOrthoPoints } = dragState; - - // Calculate offset perpendicular to segment - let offset = 0; - if (segment.orientation === 'horizontal') { - offset = mouseY - startMousePos.y; - } else { - offset = mouseX - startMousePos.x; + // Cancel previous animation frame + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); } - // No grid snapping during drag for smooth movement - // Grid snapping will be applied on mouse up + // Handle dragging - use requestAnimationFrame for smooth updates + rafRef.current = requestAnimationFrame(() => { + const svg = svgRef.current?.ownerSVGElement; + if (!svg) return; - // Update orthogonal points (local preview) - const newOrthoPoints = updateOrthogonalPointsForSegmentDrag( - originalOrthoPoints, - segment, - offset - ); + const svgRect = svg.getBoundingClientRect(); + const mouseX = e.clientX - svgRect.left; + const mouseY = e.clientY - svgRect.top; - // Update preview state (doesn't touch the store) - setPreviewOrthoPoints(newOrthoPoints); + const { segment, startMousePos, originalOrthoPoints } = dragState; + + // Calculate offset perpendicular to segment + let offset = 0; + if (segment.orientation === 'horizontal') { + offset = mouseY - startMousePos.y; + } else { + offset = mouseX - startMousePos.x; + } + + console.log('🎯 Drag Update:', { + segmentId: segment.id, + orientation: segment.orientation, + offset, + originalPointsCount: originalOrthoPoints.length, + mousePos: { x: mouseX, y: mouseY }, + }); + + // No grid snapping during drag for smooth movement + // Grid snapping will be applied on mouse up + + // Update orthogonal points (local preview) + const newOrthoPoints = updateOrthogonalPointsForSegmentDrag( + originalOrthoPoints, + segment, + offset + ); + + console.log('📍 New Ortho Points:', newOrthoPoints); + + // Update preview state (doesn't touch the store) + setPreviewOrthoPoints(newOrthoPoints); + rafRef.current = null; + }); } else if (isSelected) { // Update hovered segment const svg = svgRef.current?.ownerSVGElement; @@ -134,6 +185,18 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) const pathPoints = getPathPoints(wire); const orthoPoints = generateOrthogonalPoints(pathPoints); + console.log('🚀 Start Dragging Segment:', { + segmentId: segment.id, + orientation: segment.orientation, + segmentStart: segment.startPoint, + segmentEnd: segment.endPoint, + pathPointsCount: pathPoints.length, + orthoPointsCount: orthoPoints.length, + wireStart: wire.start, + wireEnd: wire.end, + wireControlPoints: wire.controlPoints, + }); + setDragState({ segment, startMousePos: { x: mouseX, y: mouseY }, @@ -144,6 +207,14 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) ); const handleMouseUp = useCallback(() => { + console.log('🖱️ Mouse Up - Drag State:', { + hasDragState: !!dragState, + hasPreviewPoints: !!previewOrthoPoints, + previewPointsCount: previewOrthoPoints?.length, + wireStart: wire.start, + wireEnd: wire.end, + }); + if (dragState && previewOrthoPoints) { // Apply grid snapping to final position const GRID_SIZE = 20; @@ -152,6 +223,8 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) y: Math.round(p.y / GRID_SIZE) * GRID_SIZE, })); + console.log('📐 Snapped Points:', snappedPoints); + // Convert back to control points const newControlPoints = orthogonalPointsToControlPoints( snappedPoints, @@ -159,6 +232,12 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) wire.end ); + console.log('🔧 New Control Points:', newControlPoints); + console.log('🔌 Wire Endpoints:', { + start: wire.start, + end: wire.end, + }); + // Update store only once at the end updateWire(wire.id, { controlPoints: newControlPoints }); } @@ -190,6 +269,15 @@ export const WireRenderer: React.FC = ({ wire, isSelected }) } }, [hoveredSegment, dragState]); + // Cleanup animation frame on unmount + useEffect(() => { + return () => { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + } + }; + }, []); + return ( , start: { x: number; y: number }, end: { x: number; y: number } ): WireControlPoint[] { + if (orthoPoints.length < 2) { + return []; + } + // Remove first and last points (those are start/end endpoints) const innerPoints = orthoPoints.slice(1, -1); - // Remove redundant points (those that are collinear with neighbors) + if (innerPoints.length === 0) { + return []; + } + + // Use actual orthoPoints for prev/next comparison (not start/end parameters) + const actualStart = orthoPoints[0]; + const actualEnd = orthoPoints[orthoPoints.length - 1]; + + // Keep only corner points (where direction changes) const controlPoints: WireControlPoint[] = []; for (let i = 0; i < innerPoints.length; i++) { const current = innerPoints[i]; - const prev = i === 0 ? start : innerPoints[i - 1]; - const next = i === innerPoints.length - 1 ? end : innerPoints[i + 1]; + // Get prev from orthoPoints (index i in innerPoints = index i+1 in orthoPoints) + const prev = orthoPoints[i]; // Previous point in orthoPoints + const next = orthoPoints[i + 2]; // Next point in orthoPoints // Check if current point is a corner (changes direction) const isCorner =