import React, { useEffect, useState } from "react";
import * as d3 from "d3";
function ControlPoint({ point, updateLine }) {
useEffect(() => {
const circle = d3.select(`#circle-${point.id}`);
circle.call(
d3.drag().on("drag", function (event) {
point.x = event.x;
point.y = event.y;
updateLine();
circle.attr("cx", point.x).attr("cy", point.y);
})
);
}, [point, updateLine]);
return (
<circle
id={`circle-${point.id}`}
cx={point.x}
cy={point.y}
r={5}
fill="red"
stroke="black"
/>
);
}
function LineCanvas({ lineColor, lineWidth }) {
const [lines, setLines] = useState([
{
id: 0,
type: "horizontal",
points: [
{ id: 0, x: 100, y: 300 },
{ id: 1, x: 700, y: 300 }
],
isCurve: false
},
{
id: 1,
type: "vertical",
points: [
{ id: 2, x: 400, y: 100 },
{ id: 3, x: 400, y: 500 }
],
isCurve: false
}
]);
const [history, setHistory] = useState([]);
const [isDrawing, setIsDrawing] = useState(true);
const pushToHistory = () => {
setHistory([...history, JSON.parse(JSON.stringify(lines))]);
};
const undo = () => {
if (history.length === 0) return;
const lastState = history.pop();
setLines(lastState);
};
useEffect(() => {
const svg = d3.select("#svgContainer").select("svg");
svg.on("dblclick", function (event) {
const [x, y] = d3.pointer(event);
const clickedLine = lines.find((line) =>
line.points.some(
(point) => Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5
)
);
if (clickedLine) {
clickedLine.isCurve = !clickedLine.isCurve;
redraw();
}
});
svg.on("click", function (event) {
if (!isDrawing) return;
const [x, y] = d3.pointer(event);
const lastLine = lines[lines.length - 1];
if (lastLine && !lastLine.isCurve) {
return;
}
pushToHistory();
lastLine.points.splice(lastLine.points.length - 1, 0, {
id: Date.now(),
x,
y
});
redraw();
});
document.addEventListener("keydown", function (event) {
if (event.key === "d") {
pushToHistory();
const newLineData = lines[lines.length - 1].points.map((d) => ({
id: Date.now() + d.id,
x: d.x + 20,
y: d.y + 20
}));
const newLine = { ...lines[lines.length - 1], points: newLineData };
setLines([...lines, newLine]);
} else if (event.key === "u") {
undo();
} else if (event.key === "t") {
setIsDrawing((prev) => !prev);
}
});
redraw();
}, [lines]);
const redraw = () => {
const svg = d3.select("#svgContainer").select("svg");
svg.selectAll("*").remove();
const line = d3
.line()
.x((d) => d.x)
.y((d) => d.y)
.curve(d3.curveBasis);
lines.forEach((lineData) => {
const path = svg
.append("path")
.datum(lineData.points)
.attr("d", line)
.attr("stroke", lineColor)
.attr("stroke-width", lineWidth)
.attr("fill", "none")
.call(
d3.drag().on("drag", function (event) {
lineData.points = lineData.points.map((point) => ({
x: point.x + event.dx,
y: point.y + event.dy
}));
redraw();
})
);
if (!lineData.isCurve) {
const start = lineData.points[0];
const end = lineData.points[lineData.points.length - 1];
path.attr("d", `M ${start.x},${start.y} L ${end.x},${end.y}`);
}
lineData.points.forEach((point) => {
svg
.append("circle")
.attr("cx", point.x)
.attr("cy", point.y)
.attr("r", 5)
.attr("fill", "red")
.attr("stroke", "black")
.call(
d3.drag().on("drag", function (event) {
point.x = event.x;
point.y = event.y;
redraw();
})
);
});
});
};
return (
<div id="svgContainer">
<svg width={800} height={600} />
</div>
);
}
function LineSettings({ setLineColor, setLineWidth }) {
return (
<div>
<label>
Line Color:
<input
type="color"
onChange={(e) => setLineColor(e.target.value)}
defaultValue="#000000"
/>
</label>
<label>
Line Width:
<input
type="range"
min="1"
max="10"
onChange={(e) => setLineWidth(e.target.value)}
defaultValue="2"
/>
</label>
</div>
);
}
function App() {
const [lineColor, setLineColor] = useState("black");
const [lineWidth, setLineWidth] = useState(2);
return (
<div>
<LineSettings setLineColor={setLineColor} setLineWidth={setLineWidth} />
<LineCanvas lineColor={lineColor} lineWidth={lineWidth} />
</div>
);
}
export default App;