Skip to content

Commit c7c1c9b

Browse files
author
John75SunCity
committed
feat: Add coordinate picker mode to 2D Blueprint Editor
Features: - Real-time cursor position display with crosshair lines - 'Pick Coords' button to enter coordinate picking mode - Click to mark START point, click again for END point - Shows coordinates in both inches and feet - Displays wall length calculation between points - Copy-ready coordinate values in sidebar panel - Coordinates displayed on canvas overlay Usage: 1. Click 'Pick Coords' button in toolbar 2. Click on blueprint to mark START point (green marker) 3. Click again to mark END point (red marker) 4. Copy the X/Y values from the sidebar to the Walls tab This solves the UX issue where users needed to know coordinates before they could add walls in the form view.
1 parent 1612236 commit c7c1c9b

File tree

2 files changed

+344
-5
lines changed

2 files changed

+344
-5
lines changed

records_management_3d_warehouse/static/src/components/warehouse_blueprint_editor.js

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,13 @@ export class WarehouseBlueprintEditor extends Component {
319319

320320
// Coordinate export
321321
coordinateFormat: 'json', // json, csv, geojson
322+
323+
// Cursor tracking and coordinate picking
324+
cursorPosition: { x: 0, y: 0 },
325+
showCursor: true,
326+
pickCoordinatesMode: false,
327+
pickedPoints: [], // Array of {x, y, label} for marked points
328+
pickingStep: 'start', // 'start' or 'end'
322329
});
323330

324331
// Canvas context
@@ -901,7 +908,17 @@ export class WarehouseBlueprintEditor extends Component {
901908
this.drawDragPreview(ctx);
902909
}
903910

911+
// Draw picked coordinate points
912+
if (this.state.pickedPoints.length > 0) {
913+
this.drawPickedPoints(ctx);
914+
}
915+
904916
ctx.restore();
917+
918+
// Draw cursor position indicator (outside transform for screen coordinates)
919+
if (this.state.showCursor) {
920+
this.drawCursorInfo(this.overlayCtx);
921+
}
905922
}
906923

907924
drawNavigationPath(ctx) {
@@ -1031,6 +1048,12 @@ export class WarehouseBlueprintEditor extends Component {
10311048
onMouseDown(ev) {
10321049
const pos = this.getMousePosition(ev);
10331050

1051+
// Handle coordinate picking mode
1052+
if (this.state.pickCoordinatesMode) {
1053+
this.handleCoordinatePick(pos);
1054+
return;
1055+
}
1056+
10341057
if (this.state.navigationMode) {
10351058
this.handleNavigationClick(pos);
10361059
return;
@@ -1046,10 +1069,20 @@ export class WarehouseBlueprintEditor extends Component {
10461069
onMouseMove(ev) {
10471070
const pos = this.getMousePosition(ev);
10481071

1072+
// Always update cursor position for coordinate display
1073+
this.state.cursorPosition = {
1074+
x: Math.round(pos.x),
1075+
y: Math.round(pos.y),
1076+
xFeet: Math.round(pos.x / 12 * 10) / 10,
1077+
yFeet: Math.round(pos.y / 12 * 10) / 10
1078+
};
1079+
10491080
if (this.state.isDragging) {
10501081
this.state.dragCurrent = pos;
1051-
this.renderOverlay();
10521082
}
1083+
1084+
// Always re-render overlay to show cursor position
1085+
this.renderOverlay();
10531086
}
10541087

10551088
onMouseUp(ev) {
@@ -1290,9 +1323,205 @@ export class WarehouseBlueprintEditor extends Component {
12901323
this.state.navigationMode = false;
12911324
this.state.navigationStart = null;
12921325
this.state.navigationEnd = null;
1326+
this.state.pickCoordinatesMode = false;
1327+
this.state.pickingStep = 'start';
1328+
this.renderOverlay();
1329+
}
1330+
1331+
clearPickedPoints() {
1332+
this.state.pickedPoints = [];
1333+
this.state.pickingStep = 'start';
1334+
this.renderOverlay();
1335+
}
1336+
1337+
togglePickCoordinatesMode() {
1338+
this.state.pickCoordinatesMode = !this.state.pickCoordinatesMode;
1339+
if (this.state.pickCoordinatesMode) {
1340+
this.state.mode = 'select';
1341+
this.state.pickedPoints = [];
1342+
this.state.pickingStep = 'start';
1343+
this.notification.add("Click to mark START point, then END point for wall coordinates", { type: "info" });
1344+
} else {
1345+
this.notification.add("Coordinate picking disabled", { type: "info" });
1346+
}
1347+
this.renderOverlay();
1348+
}
1349+
1350+
handleCoordinatePick(pos) {
1351+
const point = {
1352+
x: Math.round(pos.x),
1353+
y: Math.round(pos.y),
1354+
xFeet: Math.round(pos.x / 12 * 10) / 10,
1355+
yFeet: Math.round(pos.y / 12 * 10) / 10,
1356+
label: this.state.pickingStep === 'start' ? 'START' : 'END'
1357+
};
1358+
1359+
if (this.state.pickingStep === 'start') {
1360+
// Clear previous points and add start
1361+
this.state.pickedPoints = [point];
1362+
this.state.pickingStep = 'end';
1363+
this.notification.add(`START: X=${point.x}" (${point.xFeet}ft), Y=${point.y}" (${point.yFeet}ft) - Now click END point`, { type: "success" });
1364+
} else {
1365+
// Add end point
1366+
this.state.pickedPoints.push(point);
1367+
this.state.pickingStep = 'start';
1368+
1369+
const start = this.state.pickedPoints[0];
1370+
const end = point;
1371+
1372+
// Show complete wall coordinates
1373+
this.notification.add(
1374+
`Wall coordinates: Start(${start.x}, ${start.y}) → End(${end.x}, ${end.y}) | ` +
1375+
`In feet: Start(${start.xFeet}, ${start.yFeet}) → End(${end.xFeet}, ${end.yFeet})`,
1376+
{ type: "success", sticky: true }
1377+
);
1378+
}
1379+
12931380
this.renderOverlay();
12941381
}
12951382

1383+
drawPickedPoints(ctx) {
1384+
const scale = this.getScale();
1385+
1386+
for (let i = 0; i < this.state.pickedPoints.length; i++) {
1387+
const point = this.state.pickedPoints[i];
1388+
const x = point.x * scale;
1389+
const y = point.y * scale;
1390+
1391+
// Draw marker circle
1392+
ctx.beginPath();
1393+
ctx.arc(x, y, 12, 0, Math.PI * 2);
1394+
ctx.fillStyle = point.label === 'START' ? '#4CAF50' : '#F44336';
1395+
ctx.fill();
1396+
ctx.strokeStyle = '#FFFFFF';
1397+
ctx.lineWidth = 2;
1398+
ctx.stroke();
1399+
1400+
// Draw label
1401+
ctx.fillStyle = '#FFFFFF';
1402+
ctx.font = 'bold 10px Arial';
1403+
ctx.textAlign = 'center';
1404+
ctx.textBaseline = 'middle';
1405+
ctx.fillText(point.label === 'START' ? 'S' : 'E', x, y);
1406+
1407+
// Draw coordinate box
1408+
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
1409+
const coordText = `${point.x}", ${point.y}" (${point.xFeet}ft, ${point.yFeet}ft)`;
1410+
const textWidth = ctx.measureText(coordText).width + 10;
1411+
const boxY = point.label === 'START' ? y - 35 : y + 20;
1412+
1413+
ctx.fillRect(x - textWidth / 2, boxY, textWidth, 18);
1414+
ctx.fillStyle = '#FFFFFF';
1415+
ctx.font = '11px Arial';
1416+
ctx.fillText(coordText, x, boxY + 10);
1417+
1418+
// Draw line between points if we have both
1419+
if (i === 1 && this.state.pickedPoints.length === 2) {
1420+
const start = this.state.pickedPoints[0];
1421+
ctx.beginPath();
1422+
ctx.moveTo(start.x * scale, start.y * scale);
1423+
ctx.lineTo(x, y);
1424+
ctx.strokeStyle = '#2196F3';
1425+
ctx.lineWidth = 3;
1426+
ctx.setLineDash([8, 4]);
1427+
ctx.stroke();
1428+
ctx.setLineDash([]);
1429+
1430+
// Draw length label
1431+
const dx = point.x - start.x;
1432+
const dy = point.y - start.y;
1433+
const length = Math.sqrt(dx * dx + dy * dy);
1434+
const midX = (start.x * scale + x) / 2;
1435+
const midY = (start.y * scale + y) / 2;
1436+
1437+
ctx.fillStyle = 'rgba(33, 150, 243, 0.9)';
1438+
const lengthText = `Length: ${Math.round(length)}" (${Math.round(length / 12 * 10) / 10}ft)`;
1439+
const lengthWidth = ctx.measureText(lengthText).width + 10;
1440+
ctx.fillRect(midX - lengthWidth / 2, midY - 10, lengthWidth, 20);
1441+
ctx.fillStyle = '#FFFFFF';
1442+
ctx.font = 'bold 11px Arial';
1443+
ctx.fillText(lengthText, midX, midY + 4);
1444+
}
1445+
}
1446+
}
1447+
1448+
drawCursorInfo(ctx) {
1449+
const canvas = this.overlayCanvasRef.el;
1450+
const pos = this.state.cursorPosition;
1451+
1452+
// Only show if cursor is within blueprint bounds
1453+
if (!this.state.blueprint || pos.x < 0 || pos.y < 0 ||
1454+
pos.x > this.state.blueprint.length || pos.y > this.state.blueprint.width) {
1455+
return;
1456+
}
1457+
1458+
// Draw cursor crosshair on the canvas (in transformed space)
1459+
const scale = this.getScale();
1460+
const screenX = pos.x * scale * this.state.zoom + this.state.panX;
1461+
const screenY = pos.y * scale * this.state.zoom + this.state.panY;
1462+
1463+
// Crosshair lines
1464+
ctx.strokeStyle = 'rgba(33, 150, 243, 0.5)';
1465+
ctx.lineWidth = 1;
1466+
ctx.setLineDash([5, 5]);
1467+
1468+
// Vertical line
1469+
ctx.beginPath();
1470+
ctx.moveTo(screenX, 0);
1471+
ctx.lineTo(screenX, canvas.height);
1472+
ctx.stroke();
1473+
1474+
// Horizontal line
1475+
ctx.beginPath();
1476+
ctx.moveTo(0, screenY);
1477+
ctx.lineTo(canvas.width, screenY);
1478+
ctx.stroke();
1479+
1480+
ctx.setLineDash([]);
1481+
1482+
// Coordinate display box (bottom-right corner)
1483+
const boxWidth = 200;
1484+
const boxHeight = this.state.pickCoordinatesMode ? 80 : 60;
1485+
const boxX = canvas.width - boxWidth - 10;
1486+
const boxY = canvas.height - boxHeight - 10;
1487+
1488+
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
1489+
ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
1490+
1491+
ctx.fillStyle = '#FFFFFF';
1492+
ctx.font = '12px monospace';
1493+
ctx.textAlign = 'left';
1494+
1495+
// Title
1496+
ctx.font = 'bold 11px Arial';
1497+
ctx.fillText('📍 Cursor Position', boxX + 10, boxY + 16);
1498+
1499+
// Coordinates
1500+
ctx.font = '12px monospace';
1501+
ctx.fillText(`X: ${pos.x}" (${pos.xFeet} ft)`, boxX + 10, boxY + 34);
1502+
ctx.fillText(`Y: ${pos.y}" (${pos.yFeet} ft)`, boxX + 10, boxY + 50);
1503+
1504+
// Pick mode indicator
1505+
if (this.state.pickCoordinatesMode) {
1506+
ctx.fillStyle = this.state.pickingStep === 'start' ? '#4CAF50' : '#F44336';
1507+
ctx.fillText(`Click: ${this.state.pickingStep.toUpperCase()} point`, boxX + 10, boxY + 68);
1508+
}
1509+
1510+
// Axis labels on the edges
1511+
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
1512+
ctx.fillRect(screenX - 25, 5, 50, 18);
1513+
ctx.fillStyle = '#FFFFFF';
1514+
ctx.font = '10px monospace';
1515+
ctx.textAlign = 'center';
1516+
ctx.fillText(`${pos.x}"`, screenX, 17);
1517+
1518+
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
1519+
ctx.fillRect(5, screenY - 9, 50, 18);
1520+
ctx.fillStyle = '#FFFFFF';
1521+
ctx.textAlign = 'left';
1522+
ctx.fillText(`${pos.y}"`, 10, screenY + 5);
1523+
}
1524+
12961525
deleteSelectedElement() {
12971526
// TODO: Implement delete selected element
12981527
this.notification.add("Delete not yet implemented", { type: "warning" });

0 commit comments

Comments
 (0)