convert it and optimize to pure 2D. the purpose of this code is to make a schematics drawer with snapable and adjustable grid. you can remove “Z” coordinate and optimize entire code accordingly to make it pure 2D representation. you can rename title to “schematoid” if you want. I would also added a unit conversion method here, to convert all dimension to some particular specific unit, as: em, cm, m, px and rest. <–not sure if this is need right now. if you have any interesting ideas you can integrate or tell me about them. <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne? <–answer to this maybe, ne?:
<html lang=“en”><head>
<meta charset=‘UTF-8’>
<title>schematoid</title>
<style>
canvas {
display: block;
margin: auto;
}
.grid-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.grid-input{
margin: 0 10px;
}
</style>
</head>
<body>
<div class=‘grid-container’>
<label for=‘gridSize’>Grid Size: </label>
<input type=‘number’ id=‘gridSize’ class=‘grid-input’ value=‘50’>
<label for=‘gridSpacing’>Grid Spacing: </label>
<input type=‘number’ id=‘gridSpacing’ class=‘grid-input’ value=‘25’>
</div>
<canvas id=‘canvas’></canvas>
<script>
const canvas = document.getElementById(‘canvas’);
const ctx = canvas.getContext(‘2d’);

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let grid = [];
let gridSize = 50;
let gridSpacing = 25;

function updateGrid() {
grid = [];
for (let x = -gridSize; x <= gridSize; x += gridSpacing) {
for (let y = -gridSize; y <= gridSize; y += gridSpacing) {
grid.push({ x, y });
}
}
}

function updateCanvasSize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}

function updateUI() {
const gridSizeInput = document.getElementById(‘gridSize’);
const gridSpacingInput = document.getElementById(‘gridSpacing’);

gridSizeInput.value = gridSize;
gridSpacingInput.value = gridSpacing;
}

function drawLine(line) {
ctx.beginPath();
for (let i = 0; i < line.length - 1; i++) {
const projectedStart = project(rotate(line[i]));
const projectedEnd = project(rotate(line[i + 1]));

ctx.moveTo(projectedStart.x, projectedStart.y);
ctx.lineTo(projectedEnd.x, projectedEnd.y);
}

ctx.strokeStyle = ‘rgba(25,200,25,0.2)’;
ctx.lineWidth = 2;
ctx.stroke();
}

function calculateGridCenter(grid) {
let totalX = 0;
let totalY = 0;
const pointsCount = grid.length;

for (const point of grid) {
totalX += point.x;
totalY += point.y;
}

return {
x: totalX / pointsCount,
y: totalY / pointsCount
};
}

const clickedPoints = [];
let lastClickedPoint = null;
let activeLine = null;
const lines = [];

function rotate(point) {
const { x, y } = point;

return {
x: x,
y: y
};
}

function project(point) {
const { x, y } = point;

const scale = 1;
const newX = x * scale + canvas.width / 2;
const newY = y * scale + canvas.height / 2;

return { x: newX, y: newY, scale };
}

function findClosestPoint(point) {
const threshold = 20;
let minDist = Infinity;
let minIndex = -1;

for (let i = 0; i < grid.length; i++) {
const projected = project(rotate(grid[i]));
const dx = point.x - projected.x;
const dy = point.y - projected.y;
const dist = Math.sqrt(dx * dx + dy * dy);

if (dist < minDist && dist <= threshold) {
minDist = dist;
minIndex = i;
}
}

return minIndex !== -1 ? grid[minIndex] : null;
}

let mouseDown = false;
let dragStart = null;

canvas.addEventListener(‘mousedown’, (e) => {
dragStart = { x: e.clientX, y: e.clientY };
});

canvas.addEventListener(‘mousemove’, (e) => {
if (dragStart) {
const deltaX = e.clientX - dragStart.x;
const deltaY = e.clientY - dragStart.y;
dragStart = { x: e.clientX, y: e.clientY };
}
});

canvas.addEventListener(‘mouseup’, (e) => {
mouseDown = false;
dragStart = null;
if (activeLine !== null) {
const canvasRect = canvas.getBoundingClientRect();
const mouseX = e.clientX - canvasRect.left;
const mouseY = e.clientY - canvasRect.top;
const endPoint = findClosestPoint({ x: mouseX, y: mouseY });

if (endPoint !== activeLine[activeLine.length - 1]) {
activeLine.push(endPoint);
lines.push(activeLine);
}
activeLine = null;
}

if (lastClickedPoint !== null) {
const canvasRect = canvas.getBoundingClientRect();
const mouseX = e.clientX - canvasRect.left;
const mouseY = e.clientY - canvasRect.top;
const clickedPoint = findClosestPoint({ x: mouseX, y: mouseY });

if (clickedPoint !== null) {
if (lastClickedPoint === clickedPoint) {
lastClickedPoint = null;
} else {
lines.push([lastClickedPoint, clickedPoint]);
clickedPoints.push(clickedPoint);
lastClickedPoint = null;
}
}
} else {
const canvasRect = canvas.getBoundingClientRect();
const mouseX = e.clientX - canvasRect.left;
const mouseY = e.clientY - canvasRect.top;
const clickedPoint = findClosestPoint({ x: mouseX, y: mouseY });

if (clickedPoint !== null) {
lastClickedPoint = clickedPoint;
clickedPoints.push(clickedPoint);
}
}
});

function draw() {
ctx.fillStyle = ‘rgba(1,2,1,0.8)’;
ctx.fillRect(0, 0, canvas.width, canvas.height);

for (const point of grid) {
const projected = project(rotate(point));

ctx.beginPath();
if (projected.scale > 0) {
ctx.arc(projected.x, projected.y, 2 * projected.scale, 0, Math.PI * 2);
ctx.closePath();
const gradient = ctx.createRadialGradient(projected.x, projected.y, 0, projected.x, projected.y, 2 * projected.scale);
gradient.addColorStop(0, ‘rgba(255,155,255,0.5)’);
gradient.addColorStop(0.5, ‘rgba(55,255,25,0.8)’);
gradient.addColorStop(1, ‘rgba(55,25,255,0.2)’);
ctx.fillStyle = gradient;
ctx.fill();
}
}

// Drawing clicked points in green
ctx.fillStyle = ‘rgba(255,200,50,0.5)’;
for (const clickedPoint of clickedPoints) {
const projected = project(rotate(clickedPoint));

ctx.beginPath();
if (projected.scale > 0) {
ctx.arc(projected.x, projected.y, 4 * projected.scale, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
}

for (const line of lines) {
drawLine(line);
}

if (activeLine !== null) {
drawLine(activeLine);
}

requestAnimationFrame(draw);
}

window.addEventListener(‘resize’, () => {
updateCanvasSize();
updateUI();
});

const gridSizeInput = document.getElementById(‘gridSize’);
gridSizeInput.addEventListener(‘change’, (e) => {
gridSize = parseInt(e.target.value);
updateGrid();
});

const gridSpacingInput = document.getElementById(‘gridSpacing’);
gridSpacingInput.addEventListener(‘change’, (e) => {
gridSpacing = parseInt(e.target.value);
updateGrid();
});

updateUI();
updateGrid();
draw();
</script>
</body>
</html>