Skip to content

Commit 05e0214

Browse files
committed
feat: Add schematic component system with drag-and-drop functionality
BREAKING CHANGE: - Add nanoid dependency for unique component IDs - Create SchematicComponent for rendering components on canvas - Implement drag-and-drop from component palette to canvas - Add component management to schematic store - Create symbols directory for component symbol definitions - Enable canvas coordinate transformation for accurate drop positioning
1 parent 577bf74 commit 05e0214

File tree

9 files changed

+302
-10
lines changed

9 files changed

+302
-10
lines changed

package-lock.json

Lines changed: 25 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"dependencies": {
2424
"konva": "^9.3.20",
25+
"nanoid": "^5.1.5",
2526
"react": "^19.1.0",
2627
"react-dom": "^19.1.0",
2728
"react-konva": "^19.0.6",

src/components/Canvas.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import React, { useState, useRef, useEffect } from 'react'
22
import { Stage, Layer } from 'react-konva'
33
import Konva from 'konva'
44
import Grid from './Grid'
5+
import { useSchematicStore } from '../store/schematicStore'
6+
import SchematicComponent from './SchematicComponent'
57

68
const Canvas: React.FC = () => {
79
const [stageSize, setStageSize] = useState({ width: 800, height: 600 })
810
const [stageScale, setStageScale] = useState(1)
911
const [stagePosition, setStagePosition] = useState({ x: 0, y: 0 })
1012
const containerRef = useRef<HTMLDivElement>(null)
1113
const stageRef = useRef<Konva.Stage>(null)
14+
const addComponent = useSchematicStore((state) => state.addComponent)
15+
const components = useSchematicStore((state) => state.components)
1216

1317
// Handle container resizing
1418
useEffect(() => {
@@ -77,9 +81,41 @@ const Canvas: React.FC = () => {
7781
}
7882
}
7983

84+
// Handle drag over to allow drops
85+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
86+
e.preventDefault()
87+
}
88+
89+
// Handle component drop from palette
90+
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
91+
e.preventDefault()
92+
93+
// Get the library ID from the drag data
94+
const libraryId = e.dataTransfer.getData('text/plain')
95+
if (!libraryId) return
96+
97+
// Calculate drop position relative to canvas, accounting for pan/zoom
98+
const containerRect = containerRef.current?.getBoundingClientRect()
99+
if (!containerRect) return
100+
101+
// Get mouse position relative to container
102+
const dropX = e.clientX - containerRect.left
103+
const dropY = e.clientY - containerRect.top
104+
105+
// Convert screen coordinates to canvas coordinates
106+
// Account for current pan and zoom transforms
107+
const canvasX = (dropX - stagePosition.x) / stageScale
108+
const canvasY = (dropY - stagePosition.y) / stageScale
109+
110+
// Add component to store
111+
addComponent(libraryId, { x: canvasX, y: canvasY })
112+
}
113+
80114
return (
81115
<div
82116
ref={containerRef}
117+
onDragOver={handleDragOver}
118+
onDrop={handleDrop}
83119
style={{
84120
position: 'absolute',
85121
top: 0,
@@ -112,7 +148,10 @@ const Canvas: React.FC = () => {
112148
scale={stageScale}
113149
position={stagePosition}
114150
/>
115-
{/* Future: render components here */}
151+
{/* Render all components from store */}
152+
{components.map((component) => (
153+
<SchematicComponent key={component.id} component={component} />
154+
))}
116155
</Layer>
117156
</Stage>
118157
</div>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
import type { Component } from '../store/schematicStore';
3+
import { ResistorSymbol, OpAmpSymbol, MicrophoneSymbol } from './symbols';
4+
5+
interface SchematicComponentProps {
6+
component: Component;
7+
}
8+
9+
const SchematicComponent: React.FC<SchematicComponentProps> = ({ component }) => {
10+
const { libraryId, position, rotation } = component;
11+
12+
// Switch between different symbol components based on library ID
13+
switch (libraryId) {
14+
case 'RESISTOR_GENERIC':
15+
return (
16+
<ResistorSymbol
17+
x={position.x}
18+
y={position.y}
19+
rotation={rotation}
20+
/>
21+
);
22+
23+
case 'OPAMP_LM386':
24+
return (
25+
<OpAmpSymbol
26+
x={position.x}
27+
y={position.y}
28+
rotation={rotation}
29+
/>
30+
);
31+
32+
case 'ELECTRET_MICROPHONE':
33+
return (
34+
<MicrophoneSymbol
35+
x={position.x}
36+
y={position.y}
37+
rotation={rotation}
38+
/>
39+
);
40+
41+
default:
42+
// Fallback for unknown component types
43+
console.warn(`Unknown component library ID: ${libraryId}`);
44+
return null;
45+
}
46+
};
47+
48+
export default SchematicComponent;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import { Group, Circle, Line } from 'react-konva';
3+
4+
interface MicrophoneSymbolProps {
5+
x: number;
6+
y: number;
7+
rotation: number;
8+
}
9+
10+
const MicrophoneSymbol: React.FC<MicrophoneSymbolProps> = ({ x, y, rotation }) => {
11+
return (
12+
<Group x={x} y={y} rotation={rotation}>
13+
{/* Microphone capsule (circle) */}
14+
<Circle
15+
x={0}
16+
y={0}
17+
radius={12}
18+
stroke="#000000"
19+
strokeWidth={2}
20+
fill="none"
21+
/>
22+
23+
{/* Inner circle to represent capsule detail */}
24+
<Circle
25+
x={0}
26+
y={0}
27+
radius={8}
28+
stroke="#000000"
29+
strokeWidth={1}
30+
fill="none"
31+
/>
32+
33+
{/* Positive terminal line */}
34+
<Line
35+
points={[0, -12, 0, -22]}
36+
stroke="#000000"
37+
strokeWidth={2}
38+
/>
39+
40+
{/* Negative terminal line */}
41+
<Line
42+
points={[0, 12, 0, 22]}
43+
stroke="#000000"
44+
strokeWidth={2}
45+
/>
46+
47+
{/* Plus symbol for positive terminal */}
48+
<Line
49+
points={[-3, -18, 3, -18]}
50+
stroke="#000000"
51+
strokeWidth={1}
52+
/>
53+
<Line
54+
points={[0, -21, 0, -15]}
55+
stroke="#000000"
56+
strokeWidth={1}
57+
/>
58+
59+
{/* Minus symbol for negative terminal */}
60+
<Line
61+
points={[-3, 18, 3, 18]}
62+
stroke="#000000"
63+
strokeWidth={1}
64+
/>
65+
</Group>
66+
);
67+
};
68+
69+
export default MicrophoneSymbol;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { Group, Line, Text } from 'react-konva';
3+
4+
interface OpAmpSymbolProps {
5+
x: number;
6+
y: number;
7+
rotation: number;
8+
}
9+
10+
const OpAmpSymbol: React.FC<OpAmpSymbolProps> = ({ x, y, rotation }) => {
11+
return (
12+
<Group x={x} y={y} rotation={rotation}>
13+
{/* Triangle body of op-amp */}
14+
<Line
15+
points={[-20, -15, -20, 15, 20, 0, -20, -15]}
16+
stroke="#000000"
17+
strokeWidth={2}
18+
fill="none"
19+
closed={true}
20+
/>
21+
22+
{/* Positive input line */}
23+
<Line
24+
points={[-30, -8, -20, -8]}
25+
stroke="#000000"
26+
strokeWidth={2}
27+
/>
28+
29+
{/* Negative input line */}
30+
<Line
31+
points={[-30, 8, -20, 8]}
32+
stroke="#000000"
33+
strokeWidth={2}
34+
/>
35+
36+
{/* Output line */}
37+
<Line
38+
points={[20, 0, 30, 0]}
39+
stroke="#000000"
40+
strokeWidth={2}
41+
/>
42+
43+
{/* Plus symbol for positive input */}
44+
<Text
45+
x={-18}
46+
y={-12}
47+
text="+"
48+
fontSize={10}
49+
fill="#000000"
50+
fontFamily="Arial"
51+
/>
52+
53+
{/* Minus symbol for negative input */}
54+
<Text
55+
x={-18}
56+
y={4}
57+
text="−"
58+
fontSize={10}
59+
fill="#000000"
60+
fontFamily="Arial"
61+
/>
62+
</Group>
63+
);
64+
};
65+
66+
export default OpAmpSymbol;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
import { Group, Rect, Line } from 'react-konva';
3+
4+
interface ResistorSymbolProps {
5+
x: number;
6+
y: number;
7+
rotation: number;
8+
}
9+
10+
const ResistorSymbol: React.FC<ResistorSymbolProps> = ({ x, y, rotation }) => {
11+
return (
12+
<Group x={x} y={y} rotation={rotation}>
13+
{/* Left connection line */}
14+
<Line
15+
points={[-20, 0, -10, 0]}
16+
stroke="#000000"
17+
strokeWidth={2}
18+
/>
19+
20+
{/* Resistor body (rectangle) */}
21+
<Rect
22+
x={-10}
23+
y={-5}
24+
width={20}
25+
height={10}
26+
stroke="#000000"
27+
strokeWidth={2}
28+
fill="none"
29+
/>
30+
31+
{/* Right connection line */}
32+
<Line
33+
points={[10, 0, 20, 0]}
34+
stroke="#000000"
35+
strokeWidth={2}
36+
/>
37+
</Group>
38+
);
39+
};
40+
41+
export default ResistorSymbol;

src/components/symbols/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as ResistorSymbol } from './ResistorSymbol';
2+
export { default as OpAmpSymbol } from './OpAmpSymbol';
3+
export { default as MicrophoneSymbol } from './MicrophoneSymbol';

0 commit comments

Comments
 (0)