feat: enhance wire editing features with dynamic component system and improved rendering
This commit is contained in:
parent
a8bb0b6ad9
commit
85cb535804
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -293,18 +293,24 @@ Enable verbose logging:
|
||||||
- ✅ Compilation via arduino-cli to .hex files
|
- ✅ Compilation via arduino-cli to .hex files
|
||||||
- ✅ Real AVR8 emulation with avr8js
|
- ✅ Real AVR8 emulation with avr8js
|
||||||
- ✅ Pin state tracking and component updates
|
- ✅ Pin state tracking and component updates
|
||||||
- ✅ Visual components (Arduino Uno, LEDs, resistors, buttons)
|
- ✅ Dynamic component system with 48+ wokwi-elements components
|
||||||
- ✅ Wire rendering (visual only, not functional)
|
- ✅ 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:**
|
**In Progress:**
|
||||||
- 🚧 Wire editing and creation UI
|
- 🚧 Functional wire connections (electrical signal routing)
|
||||||
- 🚧 Functional wire connections (signal routing)
|
- 🚧 Wire validation and error handling
|
||||||
|
|
||||||
**Planned:**
|
**Planned:**
|
||||||
- 📋 More components (sensors, displays, motors)
|
|
||||||
- 📋 Serial monitor
|
- 📋 Serial monitor
|
||||||
- 📋 Project persistence (SQLite)
|
- 📋 Project persistence (SQLite)
|
||||||
- 📋 Multi-board support (Mega, Nano, ESP32)
|
- 📋 Multi-board support (Mega, Nano, ESP32)
|
||||||
|
- 📋 Undo/redo functionality
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
- **Monaco Code Editor** - Full VSCode-like Arduino code editing experience
|
||||||
- [@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
|
- **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
|
### Install Dependencies
|
||||||
export default defineConfig([
|
```bash
|
||||||
globalIgnores(['dist']),
|
npm install
|
||||||
{
|
|
||||||
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...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
### Run Development Server
|
||||||
|
```bash
|
||||||
```js
|
npm run dev
|
||||||
// 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...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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<Props> = ({ ... }) => {
|
||||||
|
const elementRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (elementRef.current) {
|
||||||
|
elementRef.current.setAttribute('prop', value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return <wokwi-my-component ref={elementRef} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
const [previewOrthoPoints, setPreviewOrthoPoints] = useState<Array<{ x: number; y: number }> | null>(null);
|
const [previewOrthoPoints, setPreviewOrthoPoints] = useState<Array<{ x: number; y: number }> | null>(null);
|
||||||
|
|
||||||
const svgRef = useRef<SVGGElement>(null);
|
const svgRef = useRef<SVGGElement>(null);
|
||||||
|
const rafRef = useRef<number | null>(null); // For requestAnimationFrame
|
||||||
|
|
||||||
// Generate SVG path (memoized for performance)
|
// Generate SVG path (memoized for performance)
|
||||||
// Use preview points during drag, actual wire points otherwise
|
// Use preview points during drag, actual wire points otherwise
|
||||||
|
|
@ -56,9 +57,41 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
}, [wire, previewOrthoPoints]);
|
}, [wire, previewOrthoPoints]);
|
||||||
|
|
||||||
// Compute segments (memoized)
|
// Compute segments (memoized)
|
||||||
|
// Use preview points during drag for accurate segment positions
|
||||||
const segments = useMemo(() => {
|
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);
|
return computeSegments(wire);
|
||||||
}, [wire]);
|
}, [wire, previewOrthoPoints]);
|
||||||
|
|
||||||
// Handle wire selection
|
// Handle wire selection
|
||||||
const handleWireClick = useCallback(
|
const handleWireClick = useCallback(
|
||||||
|
|
@ -73,7 +106,13 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (dragState) {
|
if (dragState) {
|
||||||
// Handle dragging - use local state for smooth updates
|
// Cancel previous animation frame
|
||||||
|
if (rafRef.current !== null) {
|
||||||
|
cancelAnimationFrame(rafRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle dragging - use requestAnimationFrame for smooth updates
|
||||||
|
rafRef.current = requestAnimationFrame(() => {
|
||||||
const svg = svgRef.current?.ownerSVGElement;
|
const svg = svgRef.current?.ownerSVGElement;
|
||||||
if (!svg) return;
|
if (!svg) return;
|
||||||
|
|
||||||
|
|
@ -91,6 +130,14 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
offset = mouseX - startMousePos.x;
|
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
|
// No grid snapping during drag for smooth movement
|
||||||
// Grid snapping will be applied on mouse up
|
// Grid snapping will be applied on mouse up
|
||||||
|
|
||||||
|
|
@ -101,8 +148,12 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
offset
|
offset
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('📍 New Ortho Points:', newOrthoPoints);
|
||||||
|
|
||||||
// Update preview state (doesn't touch the store)
|
// Update preview state (doesn't touch the store)
|
||||||
setPreviewOrthoPoints(newOrthoPoints);
|
setPreviewOrthoPoints(newOrthoPoints);
|
||||||
|
rafRef.current = null;
|
||||||
|
});
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
// Update hovered segment
|
// Update hovered segment
|
||||||
const svg = svgRef.current?.ownerSVGElement;
|
const svg = svgRef.current?.ownerSVGElement;
|
||||||
|
|
@ -134,6 +185,18 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
const pathPoints = getPathPoints(wire);
|
const pathPoints = getPathPoints(wire);
|
||||||
const orthoPoints = generateOrthogonalPoints(pathPoints);
|
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({
|
setDragState({
|
||||||
segment,
|
segment,
|
||||||
startMousePos: { x: mouseX, y: mouseY },
|
startMousePos: { x: mouseX, y: mouseY },
|
||||||
|
|
@ -144,6 +207,14 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
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) {
|
if (dragState && previewOrthoPoints) {
|
||||||
// Apply grid snapping to final position
|
// Apply grid snapping to final position
|
||||||
const GRID_SIZE = 20;
|
const GRID_SIZE = 20;
|
||||||
|
|
@ -152,6 +223,8 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
y: Math.round(p.y / GRID_SIZE) * GRID_SIZE,
|
y: Math.round(p.y / GRID_SIZE) * GRID_SIZE,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('📐 Snapped Points:', snappedPoints);
|
||||||
|
|
||||||
// Convert back to control points
|
// Convert back to control points
|
||||||
const newControlPoints = orthogonalPointsToControlPoints(
|
const newControlPoints = orthogonalPointsToControlPoints(
|
||||||
snappedPoints,
|
snappedPoints,
|
||||||
|
|
@ -159,6 +232,12 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
wire.end
|
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
|
// Update store only once at the end
|
||||||
updateWire(wire.id, { controlPoints: newControlPoints });
|
updateWire(wire.id, { controlPoints: newControlPoints });
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +269,15 @@ export const WireRenderer: React.FC<WireRendererProps> = ({ wire, isSelected })
|
||||||
}
|
}
|
||||||
}, [hoveredSegment, dragState]);
|
}, [hoveredSegment, dragState]);
|
||||||
|
|
||||||
|
// Cleanup animation frame on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (rafRef.current !== null) {
|
||||||
|
cancelAnimationFrame(rafRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
|
|
|
||||||
|
|
@ -177,22 +177,38 @@ export function updateOrthogonalPointsForSegmentDrag(
|
||||||
/**
|
/**
|
||||||
* Convert orthogonal points back to control points
|
* Convert orthogonal points back to control points
|
||||||
* Removes start/end points and intermediate points that are redundant
|
* Removes start/end points and intermediate points that are redundant
|
||||||
|
*
|
||||||
|
* IMPORTANT: The first and last orthoPoints should match the wire endpoints.
|
||||||
|
* We preserve ALL intermediate points that represent corners (direction changes).
|
||||||
*/
|
*/
|
||||||
export function orthogonalPointsToControlPoints(
|
export function orthogonalPointsToControlPoints(
|
||||||
orthoPoints: Array<{ x: number; y: number }>,
|
orthoPoints: Array<{ x: number; y: number }>,
|
||||||
start: { x: number; y: number },
|
start: { x: number; y: number },
|
||||||
end: { x: number; y: number }
|
end: { x: number; y: number }
|
||||||
): WireControlPoint[] {
|
): WireControlPoint[] {
|
||||||
|
if (orthoPoints.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Remove first and last points (those are start/end endpoints)
|
// Remove first and last points (those are start/end endpoints)
|
||||||
const innerPoints = orthoPoints.slice(1, -1);
|
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[] = [];
|
const controlPoints: WireControlPoint[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < innerPoints.length; i++) {
|
for (let i = 0; i < innerPoints.length; i++) {
|
||||||
const current = innerPoints[i];
|
const current = innerPoints[i];
|
||||||
const prev = i === 0 ? start : innerPoints[i - 1];
|
// Get prev from orthoPoints (index i in innerPoints = index i+1 in orthoPoints)
|
||||||
const next = i === innerPoints.length - 1 ? end : innerPoints[i + 1];
|
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)
|
// Check if current point is a corner (changes direction)
|
||||||
const isCorner =
|
const isCorner =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue