From bf6f1df79e1df1218f1424dfd36084565e002912 Mon Sep 17 00:00:00 2001
From: Abdo-Eid <65284648+Abdo-Eid@users.noreply.github.com>
Date: Thu, 2 Oct 2025 17:09:16 +0300
Subject: [PATCH 1/4] UI: improve hints and context menu behavior - Added close
button to hints for better UX - Improved context menu handling and grid
resizing - Prevented default browser context menu interference
---
.gitignore | 3 +
index.html | 16 +-
src/js/2d/floor2d.js | 427 ++++++------------------------------------
src/js/main.js | 5 +-
src/styles/styles.css | 36 +++-
5 files changed, 116 insertions(+), 371 deletions(-)
diff --git a/.gitignore b/.gitignore
index 924d11f..8a2fc8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ dist
# Documentation output created by jsdoc
docs/
+
+# sometimes i copy files as refrence
+ref-*
\ No newline at end of file
diff --git a/index.html b/index.html
index ec7fd69..0d4bbf5 100644
--- a/index.html
+++ b/index.html
@@ -21,6 +21,13 @@
+
diff --git a/src/js/2d/floor2d.js b/src/js/2d/floor2d.js
index 27a7292..10f33ea 100644
--- a/src/js/2d/floor2d.js
+++ b/src/js/2d/floor2d.js
@@ -6,8 +6,7 @@ export { canvas2D }
const GRID_SPACING = 30
paper.setup(canvas2D)
-let actionHistory = []
-let MAX_HISTORY_LENGTH = 50
+// #################### Grid logic ####################
/**
* createGrid creates a grid background on the Paper.js canvas.
@@ -49,7 +48,7 @@ export function createGrid(spacing = GRID_SPACING, color = '#d0d0d0') {
gridGroup.sendToBack()
gridGroup.visible = true
gridGroup.opacity = 1
- console.log('Grid created with', gridGroup.children.length, 'lines')
+ // console.log('Grid created with', gridGroup.children.length, 'lines')
return gridGroup
}
@@ -74,380 +73,74 @@ export function ensureGridVisible() {
return grid
}
-// Store the drawn paths (lines)
-let currentPath
-let currentText
-let vertices = [] // Array to store all vertices
-let contextMenu
-let selectedVertex = null // Track the selected vertex for dragging
-let selectedLine = null // Track the selected line for length adjustment
-let isDraggingVertex = false // Flag for vertex dragging
-
-function snapToGrid(point) {
- const snappedX = Math.round(point.x / GRID_SPACING) * GRID_SPACING
- const snappedY = Math.round(point.y / GRID_SPACING) * GRID_SPACING
- return new paper.Point(snappedX, snappedY)
-}
-
-function updateLengthText(path) {
- const lengthInPixels = path.length
- const metersPerPixel = 10 / paper.view.bounds.width
- const lengthInMeters = (lengthInPixels * metersPerPixel).toFixed(2)
-
- // Calculate offset perpendicular to the line to avoid overlap
- const direction = path.firstSegment.point.subtract(path.lastSegment.point)
- const offset = direction.normalize().rotate(90).multiply(30) // 30 pixel of offset
-
- currentText.content = lengthInMeters + ' m'
- currentText.point = path.getPointAt(path.length / 2).add(offset) // Offset text position
-}
-
-function updateConnectedLines(vertex) {
- vertex.data.connectedPaths.forEach((pathInfo) => {
- const path = pathInfo.path
- const index = pathInfo.index
-
- // Update the position of the path segments
- path.segments[index].point = vertex.position
-
- // Update length text
- updateLengthText(path)
- })
-}
-
-function createVertex(point) {
- const vertex = new paper.Path.Circle({
- center: point,
- radius: 5,
- fillColor: 'red',
- })
-
- vertex.data = { connectedPaths: [] } // Initialize connected paths
- vertices.push(vertex)
- return vertex
-}
-
-// Function to create the context menu
-function createContextMenu(position) {
- // Remove any existing context menu
- if (contextMenu) {
- contextMenu.remove()
- }
- // Create a new context menu
- contextMenu = document.createElement('div')
- contextMenu.style.position = 'absolute'
- contextMenu.style.top = `${position.y}px`
- contextMenu.style.left = `${position.x}px`
- contextMenu.style.background = '#fff'
- contextMenu.style.border = '1px solid #ccc'
- contextMenu.style.padding = '5px'
- contextMenu.style.boxShadow = '0px 2px 10px rgba(0, 0, 0, 0.2)'
- contextMenu.style.zIndex = 1000
-
- // Add the delete option
- const deleteOption = document.createElement('div')
- deleteOption.textContent = 'Delete Line'
- deleteOption.style.cursor = 'pointer'
- deleteOption.onclick = () => {
- if (selectedLine) {
- deleteLine(selectedLine) // Call delete function when option is clicked
- selectedLine = null
- contextMenu.remove()
- }
- }
- // Append the option to the menu
- contextMenu.appendChild(deleteOption)
- document.body.appendChild(contextMenu)
-}
-
-// Function to delete the line and its vertices
-function deleteLine(line) {
- const deletionData = {
- lineData: {
- strokeColor: line.strokeColor,
- strokeWidth: line.strokeWidth,
- },
- startPoint: line.firstSegment.point.clone(),
- endPoint: line.lastSegment.point.clone(),
- lengthText: {
- point: line.data.lengthText.point.clone(),
- content: line.data.lengthText.content,
- fillColor: line.data.lengthText.fillColor,
- fontSize: line.data.lengthText.fontSize,
- },
- }
-
- addActionToHistory({
- type: 'LINE_DELETED',
- data: deletionData,
- })
-
- // Remove connected vertices and lines from the vertex data
- vertices.forEach((vertex) => {
- vertex.data.connectedPaths = vertex.data.connectedPaths.filter(
- (pathInfo) => pathInfo.path !== line
- )
- // If the vertex has no more connected paths, remove it
- if (vertex.data.connectedPaths.length === 0) {
- vertex.remove()
- vertices.splice(vertices.indexOf(vertex), 1) // Remove vertex from the vertices array
+// #################### Context window ####################
+
+// Context menu object
+const ContextMenu = {
+ element: null,
+ options: [], // Store added options
+
+ create(position) {
+ // Remove any existing context menu
+ if (this.element) {
+ this.remove()
}
- })
-
- // Remove the line and its length text from the canvas
- if (line.data.lengthText) {
- line.data.lengthText.remove()
- }
- line.remove()
-}
-
-paper.view.onMouseDown = function (event) {
- const snappedPoint = snapToGrid(event.point) // Snap starting point to the grid
-
- if (event.event.button === 2) {
- // Perform a hit test to find the line that was right-clicked
- const hitResult = paper.project.hitTest(event.point, {
- segments: true,
- stroke: true,
- fill: true,
- tolerance: 5,
+
+ // Create a new context menu
+ this.element = document.createElement('div')
+ this.element.className = 'context-menu'
+ this.element.style.top = `${position.y}px`
+ this.element.style.left = `${position.x}px`
+
+ // Add all stored options
+ this.options.forEach(option => {
+ this.element.appendChild(option)
})
-
- if (hitResult && hitResult.item.className === 'Path') {
- selectedLine = hitResult.item
- createContextMenu(event.event) // Pass the mouse event to position the menu
- return
+
+ document.body.appendChild(this.element)
+ },
+
+ remove() {
+ if (this.element) {
+ this.element.remove()
+ this.element = null
}
- }
-
- // If not a right-click or no line is clicked, remove the context menu
- if (contextMenu) {
- contextMenu.remove()
- contextMenu = null
- }
-
- if (selectedVertex) {
- // Start a new line from the selected vertex
- if (currentPath) {
- currentPath.removeSegment(1)
+ },
+
+ addOption(text, callback) {
+ const optionElement = document.createElement('div')
+ optionElement.textContent = text
+ optionElement.className = 'context-menu-option'
+ optionElement.onclick = () => {
+ callback()
+ this.remove() // Close menu after clicking an option
}
- currentPath = new paper.Path()
- currentPath.strokeColor = 'black'
- currentPath.strokeWidth = 5
- currentPath.add(snapToGrid(selectedVertex.position)) // Snap to grid
-
- // Initialize length text
- currentText = new paper.PointText({
- point: selectedVertex.position,
- content: '',
- fillColor: 'black',
- fontSize: 16,
- })
-
- // Deselect the vertex
- selectedVertex = null
- } else if (selectedLine) {
- // Code to handle selected line (not the focus here)
- } else {
- // Start a new line normally
- currentPath = new paper.Path()
- currentPath.strokeColor = 'black'
- currentPath.strokeWidth = 5
- currentPath.add(snappedPoint) // Start at the snapped grid point
-
- // Initialize length text
- currentText = new paper.PointText({
- point: snappedPoint,
- content: '',
- fillColor: 'black',
- fontSize: 16,
- })
+ this.options.push(optionElement)
+ },
+
+ clearOptions() {
+ this.options = []
}
}
-paper.view.onMouseDrag = function (event) {
- if (currentPath) {
- // Ensure the line remains straight
- if (currentPath.segments.length > 1) {
- currentPath.removeSegment(1)
- }
- currentPath.add(snapToGrid(event.point)) // Snap endpoint to the nearest grid intersection
-
- // Update length text during drag
- updateLengthText(currentPath)
- } else if (isDraggingVertex && selectedVertex) {
- selectedVertex.position = snapToGrid(event.point) // Snap dragging vertex to grid
- updateConnectedLines(selectedVertex) // Update connected lines
- } else if (selectedLine) {
- // Adjust the length of the selected line (not the focus here)
- }
-}
-
-paper.view.onMouseUp = function () {
- if (currentPath) {
- const startVertex = snapToGrid(currentPath.firstSegment.point)
- const endVertex = snapToGrid(currentPath.lastSegment.point)
-
- currentPath.firstSegment.point = startVertex
- currentPath.lastSegment.point = endVertex
-
- // Connect path to vertices and add vertices to the list if new
- let start = vertices.find((v) => v.position.equals(startVertex))
- let end = vertices.find((v) => v.position.equals(endVertex))
-
- const wasStartVertexNew = !start
- const wasEndVertexNew = !end
+// Add options from outside the object
+// ContextMenu.addOption('Delete Line', () => {
+// if (selectedLine) {
+// deleteLine(selectedLine)
+// selectedLine = null
+// }
+// })
- if (!start) start = createVertex(startVertex)
- if (!end) end = createVertex(endVertex)
+// refresh the grid option
+ContextMenu.addOption('Close Menu', ContextMenu.remove)
- start.data.connectedPaths.push({ path: currentPath, index: 0 })
- end.data.connectedPaths.push({ path: currentPath, index: 1 })
-
- // Store the length text in the path's data for easy access
- currentPath.data.lengthText = currentText
-
- addActionToHistory({
- type: 'LINE_DRAWN',
- data: {
- line: currentPath,
- startVertex: start,
- endVertex: end,
- lengthText: currentText,
- wasStartVertexNew,
- wasEndVertexNew,
- },
- })
-
- // Reset the current path and text variables
- currentPath = null
- currentText = null
- }
-}
-
-/**
- * addActionToHistory adds the action to undo history
- *
- * @param {Object} action
- */
-function addActionToHistory(action) {
- actionHistory.push(action)
-
- if (actionHistory.length > MAX_HISTORY_LENGTH) {
- actionHistory.shift()
- }
-}
-
-/**
- * undoLineDraw undo a line drawing action
- * @param {Object} data - Contains the line and vertices information
- */
-function undoLineDraw(data) {
- const {
- line,
- startVertex,
- endVertex,
- lengthText,
- wasStartVertexNew,
- wasEndVertexNew,
- } = data
-
- if (lengthText) {
- lengthText.remove()
- }
- line.remove()
-
- if (wasStartVertexNew && startVertex) {
- const index = vertices.indexOf(startVertex)
- if (index > -1) {
- vertices.splice(index, 1)
- startVertex.remove()
- }
- } else if (startVertex) {
- startVertex.data.connectedPaths = startVertex.data.connectedPaths.filter(
- (pathInfo) => pathInfo.path !== line
- )
- }
-
- if (wasEndVertexNew && endVertex) {
- const index = vertices.indexOf(endVertex)
- if (index > -1) {
- vertices.splice(index, 1)
- endVertex.remove()
- }
- } else if (endVertex) {
- endVertex.data.connectedPaths = endVertex.data.connectedPaths.filter(
- (pathInfo) => pathInfo.path !== line
- )
- }
-
- console.log('Line drawing undone')
-}
-
-/**
- * redrawDeletedLine redo a deleted line (when undoing a delete action)
- * @param {Object} data - Contains the original line data
- */
-function redrawDeletedLine(data) {
- const { lineData, startPoint, endPoint, lengthText } = data
-
- // Recreate the line
- const newLine = new paper.Path()
- newLine.strokeColor = lineData.strokeColor
- newLine.strokeWidth = lineData.strokeWidth
- newLine.add(startPoint)
- newLine.add(endPoint)
-
- const newLengthText = new paper.PointText({
- point: lengthText.point,
- content: lengthText.content,
- fillColor: lengthText.fillColor,
- fontSize: lengthText.fontSize,
- })
-
- let startVertex = vertices.find((v) => v.position.equals(startPoint))
- let endVertex = vertices.find((v) => v.position.equals(endPoint))
-
- if (!startVertex) startVertex = createVertex(startPoint)
- if (!endVertex) endVertex = createVertex(endPoint)
-
- startVertex.data.connectedPaths.push({ path: newLine, index: 0 })
- endVertex.data.connectedPaths.push({ path: newLine, index: 1 })
-
- newLine.data.lengthText = newLengthText
-
- console.log('Deleted line restored')
-}
-
-function undo() {
- if (actionHistory.length === 0) {
- console.log('There is nothing to undo')
- return
- }
-
- const lastAction = actionHistory.pop()
-
- switch (lastAction.type) {
- case 'LINE_DRAWN':
- undoLineDraw(lastAction.data)
- break
- case 'LINE_DELETED':
- redrawDeletedLine(lastAction.data)
- break
- }
-}
-
-document.addEventListener('keydown', function (event) {
- if (
- (event.ctrlKey || event.metaKey) &&
- event.key === 'z' &&
- !event.shiftKey
- ) {
- event.preventDefault()
- undo()
- }
-})
+// Event listeners
canvas2D.addEventListener('contextmenu', (e) => {
e.preventDefault()
+ ContextMenu.create({ x: e.clientX, y: e.clientY })
})
+
+document.addEventListener('mousedown', (e) => {
+ ContextMenu.remove()
+})
\ No newline at end of file
diff --git a/src/js/main.js b/src/js/main.js
index 416c2a2..119756a 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -53,8 +53,9 @@ setupShortcutsToggle('shortcutsHeader3D', 'shortcutsContent3D')
// Handle window resizing
window.addEventListener('resize', () => {
- console.log('resizing...')
- paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight)
+ // console.log('resizing...')
+ // paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight) # replaced by resize att in html
+ createGrid()
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
diff --git a/src/styles/styles.css b/src/styles/styles.css
index 3322ec6..cfb82f6 100644
--- a/src/styles/styles.css
+++ b/src/styles/styles.css
@@ -73,6 +73,21 @@ html {
font-size: 14px;
line-height: 1.4;
z-index: 1000;
+ box-sizing: border-box;
+}
+
+.close-info {
+ position: absolute;
+ top: 8px;
+ right: 12px;
+ background: transparent;
+ border: none;
+ color: #fff;
+ font-size: 18px;
+ cursor: pointer;
+ z-index: 1100;
+ padding: 0;
+ line-height: 1;
}
#info2D {
@@ -82,7 +97,7 @@ html {
#info3D {
top: 20px;
- left: 380px;
+ left: 20vw;
}
.shortcuts-toggle {
@@ -165,4 +180,23 @@ html {
100% {
transform: rotate(360deg);
}
+}
+.context-menu {
+ position: absolute;
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 5px;
+ box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ min-width: 120px;
+}
+
+.context-menu-option {
+ cursor: pointer;
+ padding: 5px 10px;
+ border-radius: 3px;
+}
+
+.context-menu-option:hover {
+ background-color: #f0f0f0;
}
\ No newline at end of file
From 406d73bab46e936d496bb17e97f659da16ec3fbe Mon Sep 17 00:00:00 2001
From: Abdo-Eid <65284648+Abdo-Eid@users.noreply.github.com>
Date: Thu, 2 Oct 2025 21:23:17 +0300
Subject: [PATCH 2/4] Core: reimplement drawing logic and move tool - Added
move tool with Alt modifier for vertex dragging - Implemented infinite grid
(GPU-accelerated via Paper.js) - Fixed canvas resize issues when toggling
2D/3D modes
---
index.html | 1 +
src/js/2d/floor2d.js | 304 +++++++++++++++++++++++++++++++++++++++----
src/js/main.js | 3 +-
3 files changed, 279 insertions(+), 29 deletions(-)
diff --git a/index.html b/index.html
index 0d4bbf5..93da4ca 100644
--- a/index.html
+++ b/index.html
@@ -34,6 +34,7 @@
Keyboard Shortcuts ▼
- Ctrl + Z: Undo last action
+ - Alt: Move tool allow dragging vertex
diff --git a/src/js/2d/floor2d.js b/src/js/2d/floor2d.js
index 10f33ea..3722df4 100644
--- a/src/js/2d/floor2d.js
+++ b/src/js/2d/floor2d.js
@@ -3,13 +3,15 @@ import paper from 'paper'
const canvas2D = document.getElementById('canvas2D')
export { canvas2D }
-const GRID_SPACING = 30
paper.setup(canvas2D)
// #################### Grid logic ####################
+const GRID_SPACING = 30
+const GRID_RANGE = 5000 // ±10,000 units (covers huge floor plans)
+
/**
- * createGrid creates a grid background on the Paper.js canvas.
+ * createGrid creates an INFINITE grid background on the Paper.js canvas.
* @param {number} spacing
* @param {string | paper.Color} color
* @returns {paper.Group}
@@ -21,34 +23,49 @@ export function createGrid(spacing = GRID_SPACING, color = '#d0d0d0') {
existingGrid.remove()
}
- const bounds = paper.view.bounds
const gridGroup = new paper.Group()
gridGroup.name = 'grid'
- for (let x = bounds.left; x <= bounds.right; x += spacing) {
- const start = new paper.Point(x, bounds.top)
- const end = new paper.Point(x, bounds.bottom)
- const line = new paper.Path.Line(start, end)
+ // Vertical lines (x = ..., -2*spacing, -spacing, 0, spacing, 2*spacing, ...)
+ const startX = -GRID_RANGE
+ const endX = GRID_RANGE
+ for (
+ let x = Math.floor(startX / spacing) * spacing;
+ x <= endX;
+ x += spacing
+ ) {
+ const line = new paper.Path.Line(
+ new paper.Point(x, -GRID_RANGE),
+ new paper.Point(x, GRID_RANGE)
+ )
line.strokeColor = color
line.strokeWidth = 1
- line.opacity = 0.7 // More visible
+ line.opacity = 0.7
gridGroup.addChild(line)
}
- for (let y = bounds.top; y <= bounds.bottom; y += spacing) {
- const start = new paper.Point(bounds.left, y)
- const end = new paper.Point(bounds.right, y)
- const line = new paper.Path.Line(start, end)
+ // Horizontal lines (y = ..., -2*spacing, -spacing, 0, spacing, 2*spacing, ...)
+ const startY = -GRID_RANGE
+ const endY = GRID_RANGE
+ for (
+ let y = Math.floor(startY / spacing) * spacing;
+ y <= endY;
+ y += spacing
+ ) {
+ const line = new paper.Path.Line(
+ new paper.Point(-GRID_RANGE, y),
+ new paper.Point(GRID_RANGE, y)
+ )
line.strokeColor = color
line.strokeWidth = 1
- line.opacity = 0.7 // More visible
+ line.opacity = 0.7
gridGroup.addChild(line)
}
gridGroup.sendToBack()
gridGroup.visible = true
gridGroup.opacity = 1
- // console.log('Grid created with', gridGroup.children.length, 'lines')
+ // console.log('Infinite grid created with', gridGroup.children.length, 'lines')
return gridGroup
}
@@ -57,12 +74,9 @@ export function createGrid(spacing = GRID_SPACING, color = '#d0d0d0') {
* @returns {paper.Group}
*/
export function ensureGridVisible() {
- /**
- * @type {paper.Item | null}
- */
let grid = paper.project.getItem({ name: 'grid' })
if (!grid) {
- console.log('No grid found, creating new one')
+ console.log('No grid found, creating new infinite grid')
grid = createGrid()
} else {
grid.visible = true
@@ -79,13 +93,13 @@ export function ensureGridVisible() {
const ContextMenu = {
element: null,
options: [], // Store added options
-
+
create(position) {
// Remove any existing context menu
if (this.element) {
this.remove()
}
-
+
// Create a new context menu
this.element = document.createElement('div')
this.element.className = 'context-menu'
@@ -93,20 +107,20 @@ const ContextMenu = {
this.element.style.left = `${position.x}px`
// Add all stored options
- this.options.forEach(option => {
+ this.options.forEach((option) => {
this.element.appendChild(option)
})
-
+
document.body.appendChild(this.element)
},
-
+
remove() {
if (this.element) {
this.element.remove()
this.element = null
}
},
-
+
addOption(text, callback) {
const optionElement = document.createElement('div')
optionElement.textContent = text
@@ -117,10 +131,10 @@ const ContextMenu = {
}
this.options.push(optionElement)
},
-
+
clearOptions() {
this.options = []
- }
+ },
}
// Add options from outside the object
@@ -134,7 +148,6 @@ const ContextMenu = {
// refresh the grid option
ContextMenu.addOption('Close Menu', ContextMenu.remove)
-
// Event listeners
canvas2D.addEventListener('contextmenu', (e) => {
e.preventDefault()
@@ -143,4 +156,239 @@ canvas2D.addEventListener('contextmenu', (e) => {
document.addEventListener('mousedown', (e) => {
ContextMenu.remove()
-})
\ No newline at end of file
+})
+
+// #################### Draw logic ####################
+let isPanning = false
+let lastPanPoint = null
+let startPoint = null
+let tempLine = null
+let tempText = null
+let snapIndicator = null
+let isDragging = false
+let selectedVertex = null
+let isDraggingVertex = false
+let vertices = []
+const LINE_STROKE_WIDTH = 3
+const MIN_DRAG_DISTANCE = 5
+
+/**
+ * Snap a point to the grid
+ */
+function snapToGrid(point) {
+ return new paper.Point(
+ Math.round(point.x / GRID_SPACING) * GRID_SPACING,
+ Math.round(point.y / GRID_SPACING) * GRID_SPACING
+ )
+}
+
+/**
+ * Create a red vertex circle at given point
+ */
+function createVertex(point) {
+ const vertex = new paper.Path.Circle({
+ center: point,
+ radius: 6,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 1,
+ })
+ vertex.data = { connectedPaths: [] }
+ vertices.push(vertex)
+ vertex.bringToFront()
+ return vertex
+}
+
+/**
+ * Update length text
+ */
+function updateLengthText(path, textItem) {
+ if (!textItem) return
+
+ const lengthInPixels = path.length
+ const lengthInMeters = (lengthInPixels / 100).toFixed(2)
+
+ const direction = path.firstSegment.point.subtract(path.lastSegment.point)
+ if (direction.length === 0) return
+ const offset = direction.normalize().rotate(90).multiply(20)
+
+ textItem.content = `${lengthInMeters} m`
+ textItem.point = path.getPointAt(path.length / 2).add(offset)
+}
+
+function cleanupPreview() {
+ if (tempLine) {
+ tempLine.remove()
+ tempLine = null
+ }
+ if (tempText) {
+ tempText.remove()
+ tempText = null
+ }
+ if (snapIndicator) {
+ snapIndicator.remove()
+ snapIndicator = null
+ }
+}
+
+paper.view.onMouseDown = (event) => {
+ isDragging = false
+ isDraggingVertex = false
+
+ // ✅ Read Alt state from mouse event
+ const isAltPressed = event.event.altKey
+
+ const hitResult = paper.project.hitTest(event.point, {
+ fill: true,
+ stroke: true,
+ segments: true,
+ tolerance: 12,
+ match: (hit) => hit.item && vertices.includes(hit.item),
+ })
+
+ if (hitResult?.item && vertices.includes(hitResult.item)) {
+ if (isAltPressed) {
+ selectedVertex = hitResult.item
+ startPoint = null
+ } else {
+ startPoint = hitResult.item.position.clone()
+ selectedVertex = null
+ }
+ } else {
+ // Not clicking on a vertex
+ if (isAltPressed) {
+ // Start panning
+ isPanning = true
+ canvas2D.style.cursor = 'grab'
+ lastPanPoint = event.point.clone()
+ } else {
+ // Start drawing
+ selectedVertex = null
+ startPoint = event.point.clone()
+ }
+ }
+}
+
+paper.view.onMouseDrag = (event) => {
+ const isAltPressed = event.event.altKey
+
+ if (isPanning && lastPanPoint) {
+ canvas2D.style.cursor = 'grabbing'
+ paper.view.translate(event.point.subtract(lastPanPoint))
+ lastPanPoint = event.point.clone()
+
+ }
+
+ const dragDistance = selectedVertex
+ ? selectedVertex.position.subtract(event.point).length
+ : startPoint
+ ? startPoint.subtract(event.point).length
+ : 0
+
+ if (dragDistance < MIN_DRAG_DISTANCE) return
+
+ if (isAltPressed && selectedVertex) {
+ isDraggingVertex = true
+ cleanupPreview()
+
+ const snappedPos = snapToGrid(event.point)
+ selectedVertex.position = snappedPos
+
+ selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
+ path.segments[index].point = snappedPos
+ if (path.data?.lengthText) {
+ updateLengthText(path, path.data.lengthText)
+ }
+ })
+ } else if (startPoint) {
+ isDragging = true
+ cleanupPreview()
+
+ const snappedStart = snapToGrid(startPoint)
+ const snappedEnd = snapToGrid(event.point)
+
+ snapIndicator = new paper.Path.Circle({
+ center: snappedEnd,
+ radius: 4,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 0.5,
+ opacity: 0.8,
+ })
+
+ tempLine = new paper.Path.Line({
+ from: snappedStart,
+ to: snappedEnd,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ dashArray: [6, 4],
+ opacity: 0.6,
+ })
+
+ tempText = new paper.PointText({
+ point: snappedStart,
+ content: '',
+ fillColor: 'black',
+ fontSize: 14,
+ fontFamily: 'Arial',
+ })
+ updateLengthText(tempLine, tempText)
+ }
+}
+
+paper.view.onMouseUp = (event) => {
+ const isAltPressed = event.event.altKey // ✅ From mouse event
+ cleanupPreview()
+ if (isPanning) {
+ isPanning = false
+ lastPanPoint = null
+ canvas2D.style.cursor = 'default'
+ }
+
+ if (isDraggingVertex && isAltPressed && selectedVertex) {
+ const snappedPos = snapToGrid(selectedVertex.position)
+ selectedVertex.position = snappedPos
+ selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
+ path.segments[index].point = snappedPos
+ if (path.data?.lengthText) {
+ updateLengthText(path, path.data.lengthText)
+ }
+ })
+ } else if (isDragging && startPoint) {
+ const snappedStart = snapToGrid(startPoint)
+ const snappedEnd = snapToGrid(event.point)
+
+ if (!snappedStart.equals(snappedEnd)) {
+ let startVertex = vertices.find((v) => v.position.equals(snappedStart))
+ let endVertex = vertices.find((v) => v.position.equals(snappedEnd))
+
+ if (!startVertex) startVertex = createVertex(snappedStart)
+ if (!endVertex) endVertex = createVertex(snappedEnd)
+
+ const finalLine = new paper.Path.Line({
+ from: snappedStart,
+ to: snappedEnd,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ })
+
+ const finalText = new paper.PointText({
+ point: snappedStart,
+ content: '',
+ fillColor: 'black',
+ fontSize: 14,
+ fontFamily: 'Arial',
+ })
+ updateLengthText(finalLine, finalText)
+ finalLine.data = { lengthText: finalText }
+
+ startVertex.data.connectedPaths.push({ path: finalLine, index: 0 })
+ endVertex.data.connectedPaths.push({ path: finalLine, index: 1 })
+ }
+ }
+
+ startPoint = null
+ selectedVertex = null
+ isDragging = false
+ isDraggingVertex = false
+}
diff --git a/src/js/main.js b/src/js/main.js
index 119756a..b627e0a 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -24,6 +24,8 @@ switchButton.addEventListener('click', () => {
container2D.style.display = 'block'
container3D.style.display = 'none'
switchButton.textContent = 'Switch to 3D'
+ // fixes when window is small that part of floorplan is cutted then turn to 3D the window is big again then turn to 2d the canvas is striched not resized
+ paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight)
}
})
@@ -55,7 +57,6 @@ setupShortcutsToggle('shortcutsHeader3D', 'shortcutsContent3D')
window.addEventListener('resize', () => {
// console.log('resizing...')
// paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight) # replaced by resize att in html
- createGrid()
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
From 587ff2faabf73946137fc89f321eb393fe02efe5 Mon Sep 17 00:00:00 2001
From: Abdo-Eid <65284648+Abdo-Eid@users.noreply.github.com>
Date: Fri, 3 Oct 2025 01:10:06 +0300
Subject: [PATCH 3/4] Refactor: modularize Floor2D system - Split monolithic
code into modules - Improved panning stability and performance
---
index.html | 1 +
src/config.js | 0
src/js/2d/contextMenu2d.js | 75 +++++++
src/js/2d/core2d.js | 112 +++++++++++
src/js/2d/drawing2d.js | 243 +++++++++++++++++++++++
src/js/2d/dxfLoader.js | 2 +-
src/js/2d/floor2d.js | 394 -------------------------------------
src/js/main.js | 35 ++--
8 files changed, 455 insertions(+), 407 deletions(-)
create mode 100644 src/config.js
create mode 100644 src/js/2d/contextMenu2d.js
create mode 100644 src/js/2d/core2d.js
create mode 100644 src/js/2d/drawing2d.js
delete mode 100644 src/js/2d/floor2d.js
diff --git a/index.html b/index.html
index 93da4ca..62c9d97 100644
--- a/index.html
+++ b/index.html
@@ -35,6 +35,7 @@
Keyboard Shortcuts ▼
- Ctrl + Z: Undo last action
- Alt: Move tool allow dragging vertex
+ - Alt + grab: navigate the canvas
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/js/2d/contextMenu2d.js b/src/js/2d/contextMenu2d.js
new file mode 100644
index 0000000..7390b4d
--- /dev/null
+++ b/src/js/2d/contextMenu2d.js
@@ -0,0 +1,75 @@
+// Context menu object
+const ContextMenu = {
+ element: null,
+ options: [], // Store added options
+
+ create(position) {
+ // Remove any existing context menu
+ this.remove()
+
+ // Create a new context menu
+ this.element = document.createElement('div')
+ this.element.className = 'context-menu'
+ this.element.style.top = `${position.y}px`
+ this.element.style.left = `${position.x}px`
+
+ // Add all stored options
+ this.options.forEach((option) => {
+ this.element.appendChild(option)
+ })
+
+ document.body.appendChild(this.element)
+ },
+
+ remove() {
+ if (this.element) {
+ this.element.remove()
+ this.element = null
+ }
+ },
+
+ addOption(text, callback) {
+ const optionElement = document.createElement('div')
+ optionElement.textContent = text
+ optionElement.className = 'context-menu-option'
+ optionElement.onclick = () => {
+ callback()
+ this.remove() // Close menu after clicking an option
+ }
+ this.options.push(optionElement)
+ },
+
+ clearOptions() {
+ this.options = []
+ },
+}
+
+// Default option
+ContextMenu.addOption('Close Menu', () => ContextMenu.remove())
+
+// ==========================================
+// Safe initialization function
+// ==========================================
+export function initContextMenu(canvas2D) {
+ if (!canvas2D) {
+ console.warn('initContextMenu: canvas2D not found')
+ return
+ }
+
+ // Right-click opens custom menu
+ canvas2D.addEventListener('contextmenu', (e) => {
+ e.preventDefault()
+ ContextMenu.create({ x: e.clientX, y: e.clientY })
+ })
+
+ // Click anywhere else closes it
+ document.addEventListener('mousedown', (e) => {
+ if (ContextMenu.element && !ContextMenu.element.contains(e.target)) {
+ ContextMenu.remove()
+ }
+ })
+
+ console.log('Context menu initialized')
+}
+
+export { ContextMenu }
diff --git a/src/js/2d/core2d.js b/src/js/2d/core2d.js
new file mode 100644
index 0000000..16a0cec
--- /dev/null
+++ b/src/js/2d/core2d.js
@@ -0,0 +1,112 @@
+import paper from 'paper'
+
+import { setupMouseHandlers } from './drawing2d.js'
+import { initContextMenu } from './contextMenu2d.js'
+
+// ----------------------------------------------------
+// 1. Canvas and Paper.js Setup
+// ----------------------------------------------------
+const canvas2D = document.getElementById('canvas2D')
+paper.setup(canvas2D)
+
+// ----------------------------------------------------
+// 2. Constants
+// ----------------------------------------------------
+export const GRID_SPACING = 30
+export const LINE_STROKE_WIDTH = 3
+export const MIN_DRAG_DISTANCE = 5
+
+// ----------------------------------------------------
+// 3. Grid Management
+// ----------------------------------------------------
+let gridGroup = null
+let currentGridSpacing = GRID_SPACING
+let currentGridColor = '#d0d0d0'
+
+/**
+ * Create the 2D grid based on view bounds
+ */
+export function createGrid(spacing = GRID_SPACING, color = '#d0d0d0') {
+ currentGridSpacing = spacing
+ currentGridColor = color
+
+ if (!gridGroup) {
+ gridGroup = new paper.Group()
+ gridGroup.name = 'grid'
+ } else {
+ gridGroup.removeChildren()
+ }
+
+ const viewBounds = paper.view.bounds
+ const left = Math.floor(viewBounds.left / spacing) * spacing
+ const right = Math.ceil(viewBounds.right / spacing) * spacing
+ const top = Math.floor(viewBounds.top / spacing) * spacing
+ const bottom = Math.ceil(viewBounds.bottom / spacing) * spacing
+
+ // Vertical lines
+ for (let x = left; x <= right; x += spacing) {
+ const line = new paper.Path.Line(
+ new paper.Point(x, top),
+ new paper.Point(x, bottom)
+ )
+ line.strokeColor = color
+ line.strokeWidth = 1
+ line.opacity = 0.7
+ gridGroup.addChild(line)
+ }
+
+ // Horizontal lines
+ for (let y = top; y <= bottom; y += spacing) {
+ const line = new paper.Path.Line(
+ new paper.Point(left, y),
+ new paper.Point(right, y)
+ )
+ line.strokeColor = color
+ line.strokeWidth = 1
+ line.opacity = 0.7
+ gridGroup.addChild(line)
+ }
+
+ gridGroup.sendToBack()
+ return gridGroup
+}
+
+/**
+ * Ensure grid is visible and created
+ */
+export function ensureGridVisible() {
+ if (!gridGroup) {
+ createGrid()
+ } else {
+ gridGroup.visible = true
+ }
+ return gridGroup
+}
+
+// Auto-refresh grid when view changes
+paper.view.onFrame = function () {
+ if (gridGroup) {
+ createGrid(currentGridSpacing, currentGridColor)
+ }
+}
+
+// ----------------------------------------------------
+// 4. Initialization (replaces floor2d.js)
+// ----------------------------------------------------
+export function setup2D() {
+ // Setup drawing handlers
+ setupMouseHandlers(canvas2D)
+
+ // Initialize context menu
+ initContextMenu(canvas2D)
+
+ // Create grid initially
+ createGrid()
+
+ console.log('2D environment initialized')
+}
+
+// ----------------------------------------------------
+// 5. Exports
+// ----------------------------------------------------
+export { paper, canvas2D }
diff --git a/src/js/2d/drawing2d.js b/src/js/2d/drawing2d.js
new file mode 100644
index 0000000..3d9f442
--- /dev/null
+++ b/src/js/2d/drawing2d.js
@@ -0,0 +1,243 @@
+import { paper, LINE_STROKE_WIDTH, MIN_DRAG_DISTANCE, GRID_SPACING } from './core2d.js'
+
+let vertices = []
+
+/**
+ * Snap a point to the grid
+ */
+export function snapToGrid(point) {
+ return new paper.Point(
+ Math.round(point.x / GRID_SPACING) * GRID_SPACING,
+ Math.round(point.y / GRID_SPACING) * GRID_SPACING
+ )
+}
+
+/**
+ * Create a red vertex circle at given point
+ */
+export function createVertex(point) {
+ const vertex = new paper.Path.Circle({
+ center: point,
+ radius: 6,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 1,
+ })
+ vertex.data = { connectedPaths: [] }
+ vertices.push(vertex)
+ vertex.bringToFront()
+ return vertex
+}
+
+export function getVertices() {
+ return vertices
+}
+
+export function clearVertices() {
+ vertices.forEach((vertex) => vertex.remove())
+ vertices = []
+}
+
+
+let isPanning = false
+let startPoint = null
+let tempLine = null
+let tempText = null
+let snapIndicator = null
+let isDragging = false
+let selectedVertex = null
+let isDraggingVertex = false
+
+/**
+ * Update length text
+ */
+function updateLengthText(path, textItem) {
+ if (!textItem) return
+
+ const lengthInPixels = path.length
+ const metersPerPixel = 10 / paper.view.bounds.width
+ const lengthInMeters = (lengthInPixels * metersPerPixel).toFixed(2)
+
+ const direction = path.firstSegment.point.subtract(path.lastSegment.point)
+ if (direction.length === 0) return
+ const offset = direction.normalize().rotate(90).multiply(20)
+
+ textItem.content = `${lengthInMeters} m`
+ textItem.point = path.getPointAt(path.length / 2).add(offset)
+}
+
+function cleanupPreview() {
+ if (tempLine) {
+ tempLine.remove()
+ tempLine = null
+ }
+ if (tempText) {
+ tempText.remove()
+ tempText = null
+ }
+ if (snapIndicator) {
+ snapIndicator.remove()
+ snapIndicator = null
+ }
+}
+
+// Mouse event handlers
+export function setupMouseHandlers(canvas2D) {
+ paper.view.onMouseDown = (event) => {
+ isDragging = false
+ isDraggingVertex = false
+
+ // ✅ Read Alt state from mouse event
+ const isAltPressed = event.event.altKey
+
+ const hitResult = paper.project.hitTest(event.point, {
+ fill: true,
+ stroke: true,
+ segments: true,
+ tolerance: 12,
+ match: (hit) => hit.item && getVertices().includes(hit.item),
+ })
+
+ if (hitResult?.item && getVertices().includes(hitResult.item)) {
+ if (isAltPressed) {
+ selectedVertex = hitResult.item
+ startPoint = null
+ } else {
+ startPoint = hitResult.item.position.clone()
+ selectedVertex = null
+ }
+ } else {
+ // Not clicking on a vertex
+ if (isAltPressed) {
+ // Start panning
+ isPanning = true
+ canvas2D.style.cursor = 'grab'
+ } else {
+ // Start drawing
+ selectedVertex = null
+ startPoint = event.point.clone()
+ }
+ }
+ }
+
+ paper.view.onMouseDrag = (event) => {
+ const isAltPressed = event.event.altKey
+
+ if (isPanning) {
+ canvas2D.style.cursor = 'grabbing'
+ paper.view.translate(event.delta.divide(2)) // the divition for stability
+ }
+
+ const dragDistance = selectedVertex
+ ? selectedVertex.position.subtract(event.point).length
+ : startPoint
+ ? startPoint.subtract(event.point).length
+ : 0
+
+ if (dragDistance < MIN_DRAG_DISTANCE) return
+
+ if (isAltPressed && selectedVertex) {
+ isDraggingVertex = true
+ cleanupPreview()
+
+ const snappedPos = snapToGrid(event.point)
+ selectedVertex.position = snappedPos
+
+ selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
+ path.segments[index].point = snappedPos
+ if (path.data?.lengthText) {
+ updateLengthText(path, path.data.lengthText)
+ }
+ })
+ } else if (startPoint) {
+ isDragging = true
+ cleanupPreview()
+
+ const snappedStart = snapToGrid(startPoint)
+ const snappedEnd = snapToGrid(event.point)
+
+ snapIndicator = new paper.Path.Circle({
+ center: snappedEnd,
+ radius: 4,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 0.5,
+ opacity: 0.8,
+ })
+
+ tempLine = new paper.Path.Line({
+ from: snappedStart,
+ to: snappedEnd,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ dashArray: [6, 4],
+ opacity: 0.6,
+ })
+
+ tempText = new paper.PointText({
+ point: snappedStart,
+ content: '',
+ fillColor: 'black',
+ fontSize: 14,
+ fontFamily: 'Arial',
+ })
+ updateLengthText(tempLine, tempText)
+ }
+ }
+
+ paper.view.onMouseUp = (event) => {
+ const isAltPressed = event.event.altKey // ✅ From mouse event
+ cleanupPreview()
+ if (isPanning) {
+ isPanning = false
+ canvas2D.style.cursor = 'default'
+ }
+
+ if (isDraggingVertex && isAltPressed && selectedVertex) {
+ const snappedPos = snapToGrid(selectedVertex.position)
+ selectedVertex.position = snappedPos
+ selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
+ path.segments[index].point = snappedPos
+ if (path.data?.lengthText) {
+ updateLengthText(path, path.data.lengthText)
+ }
+ })
+ } else if (isDragging && startPoint) {
+ const snappedStart = snapToGrid(startPoint)
+ const snappedEnd = snapToGrid(event.point)
+
+ if (!snappedStart.equals(snappedEnd)) {
+ let startVertex = getVertices().find((v) => v.position.equals(snappedStart))
+ let endVertex = getVertices().find((v) => v.position.equals(snappedEnd))
+
+ if (!startVertex) startVertex = createVertex(snappedStart)
+ if (!endVertex) endVertex = createVertex(snappedEnd)
+
+ const finalLine = new paper.Path.Line({
+ from: snappedStart,
+ to: snappedEnd,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ })
+
+ const finalText = new paper.PointText({
+ point: snappedStart,
+ content: '',
+ fillColor: 'black',
+ fontSize: 14,
+ fontFamily: 'Arial',
+ })
+ updateLengthText(finalLine, finalText)
+ finalLine.data = { lengthText: finalText }
+
+ startVertex.data.connectedPaths.push({ path: finalLine, index: 0 })
+ endVertex.data.connectedPaths.push({ path: finalLine, index: 1 })
+ }
+ }
+
+ startPoint = null
+ selectedVertex = null
+ isDragging = false
+ isDraggingVertex = false
+ }
+}
\ No newline at end of file
diff --git a/src/js/2d/dxfLoader.js b/src/js/2d/dxfLoader.js
index 5d2f6a4..a1f2ca2 100644
--- a/src/js/2d/dxfLoader.js
+++ b/src/js/2d/dxfLoader.js
@@ -1,5 +1,5 @@
import paper from 'paper'
-import { ensureGridVisible } from './floor2d.js'
+import { ensureGridVisible } from './core2d.js'
/**
* loadDxfFile loads and parses a DXF file, scales its entities to fit the canvas,
diff --git a/src/js/2d/floor2d.js b/src/js/2d/floor2d.js
deleted file mode 100644
index 3722df4..0000000
--- a/src/js/2d/floor2d.js
+++ /dev/null
@@ -1,394 +0,0 @@
-import paper from 'paper'
-
-const canvas2D = document.getElementById('canvas2D')
-export { canvas2D }
-
-paper.setup(canvas2D)
-
-// #################### Grid logic ####################
-
-const GRID_SPACING = 30
-const GRID_RANGE = 5000 // ±10,000 units (covers huge floor plans)
-
-/**
- * createGrid creates an INFINITE grid background on the Paper.js canvas.
- * @param {number} spacing
- * @param {string | paper.Color} color
- * @returns {paper.Group}
- */
-export function createGrid(spacing = GRID_SPACING, color = '#d0d0d0') {
- // Remove existing grid first
- const existingGrid = paper.project.getItem({ name: 'grid' })
- if (existingGrid) {
- existingGrid.remove()
- }
-
- const gridGroup = new paper.Group()
- gridGroup.name = 'grid'
-
- // Vertical lines (x = ..., -2*spacing, -spacing, 0, spacing, 2*spacing, ...)
- const startX = -GRID_RANGE
- const endX = GRID_RANGE
- for (
- let x = Math.floor(startX / spacing) * spacing;
- x <= endX;
- x += spacing
- ) {
- const line = new paper.Path.Line(
- new paper.Point(x, -GRID_RANGE),
- new paper.Point(x, GRID_RANGE)
- )
- line.strokeColor = color
- line.strokeWidth = 1
- line.opacity = 0.7
- gridGroup.addChild(line)
- }
-
- // Horizontal lines (y = ..., -2*spacing, -spacing, 0, spacing, 2*spacing, ...)
- const startY = -GRID_RANGE
- const endY = GRID_RANGE
- for (
- let y = Math.floor(startY / spacing) * spacing;
- y <= endY;
- y += spacing
- ) {
- const line = new paper.Path.Line(
- new paper.Point(-GRID_RANGE, y),
- new paper.Point(GRID_RANGE, y)
- )
- line.strokeColor = color
- line.strokeWidth = 1
- line.opacity = 0.7
- gridGroup.addChild(line)
- }
-
- gridGroup.sendToBack()
- gridGroup.visible = true
- gridGroup.opacity = 1
- // console.log('Infinite grid created with', gridGroup.children.length, 'lines')
- return gridGroup
-}
-
-/**
- * ensureGridVisible ensures that a grid is visible on the canvas.
- * @returns {paper.Group}
- */
-export function ensureGridVisible() {
- let grid = paper.project.getItem({ name: 'grid' })
- if (!grid) {
- console.log('No grid found, creating new infinite grid')
- grid = createGrid()
- } else {
- grid.visible = true
- grid.opacity = 1
- grid.sendToBack()
- console.log('Grid made visible')
- }
- return grid
-}
-
-// #################### Context window ####################
-
-// Context menu object
-const ContextMenu = {
- element: null,
- options: [], // Store added options
-
- create(position) {
- // Remove any existing context menu
- if (this.element) {
- this.remove()
- }
-
- // Create a new context menu
- this.element = document.createElement('div')
- this.element.className = 'context-menu'
- this.element.style.top = `${position.y}px`
- this.element.style.left = `${position.x}px`
-
- // Add all stored options
- this.options.forEach((option) => {
- this.element.appendChild(option)
- })
-
- document.body.appendChild(this.element)
- },
-
- remove() {
- if (this.element) {
- this.element.remove()
- this.element = null
- }
- },
-
- addOption(text, callback) {
- const optionElement = document.createElement('div')
- optionElement.textContent = text
- optionElement.className = 'context-menu-option'
- optionElement.onclick = () => {
- callback()
- this.remove() // Close menu after clicking an option
- }
- this.options.push(optionElement)
- },
-
- clearOptions() {
- this.options = []
- },
-}
-
-// Add options from outside the object
-// ContextMenu.addOption('Delete Line', () => {
-// if (selectedLine) {
-// deleteLine(selectedLine)
-// selectedLine = null
-// }
-// })
-
-// refresh the grid option
-ContextMenu.addOption('Close Menu', ContextMenu.remove)
-
-// Event listeners
-canvas2D.addEventListener('contextmenu', (e) => {
- e.preventDefault()
- ContextMenu.create({ x: e.clientX, y: e.clientY })
-})
-
-document.addEventListener('mousedown', (e) => {
- ContextMenu.remove()
-})
-
-// #################### Draw logic ####################
-let isPanning = false
-let lastPanPoint = null
-let startPoint = null
-let tempLine = null
-let tempText = null
-let snapIndicator = null
-let isDragging = false
-let selectedVertex = null
-let isDraggingVertex = false
-let vertices = []
-const LINE_STROKE_WIDTH = 3
-const MIN_DRAG_DISTANCE = 5
-
-/**
- * Snap a point to the grid
- */
-function snapToGrid(point) {
- return new paper.Point(
- Math.round(point.x / GRID_SPACING) * GRID_SPACING,
- Math.round(point.y / GRID_SPACING) * GRID_SPACING
- )
-}
-
-/**
- * Create a red vertex circle at given point
- */
-function createVertex(point) {
- const vertex = new paper.Path.Circle({
- center: point,
- radius: 6,
- fillColor: 'red',
- strokeColor: 'darkred',
- strokeWidth: 1,
- })
- vertex.data = { connectedPaths: [] }
- vertices.push(vertex)
- vertex.bringToFront()
- return vertex
-}
-
-/**
- * Update length text
- */
-function updateLengthText(path, textItem) {
- if (!textItem) return
-
- const lengthInPixels = path.length
- const lengthInMeters = (lengthInPixels / 100).toFixed(2)
-
- const direction = path.firstSegment.point.subtract(path.lastSegment.point)
- if (direction.length === 0) return
- const offset = direction.normalize().rotate(90).multiply(20)
-
- textItem.content = `${lengthInMeters} m`
- textItem.point = path.getPointAt(path.length / 2).add(offset)
-}
-
-function cleanupPreview() {
- if (tempLine) {
- tempLine.remove()
- tempLine = null
- }
- if (tempText) {
- tempText.remove()
- tempText = null
- }
- if (snapIndicator) {
- snapIndicator.remove()
- snapIndicator = null
- }
-}
-
-paper.view.onMouseDown = (event) => {
- isDragging = false
- isDraggingVertex = false
-
- // ✅ Read Alt state from mouse event
- const isAltPressed = event.event.altKey
-
- const hitResult = paper.project.hitTest(event.point, {
- fill: true,
- stroke: true,
- segments: true,
- tolerance: 12,
- match: (hit) => hit.item && vertices.includes(hit.item),
- })
-
- if (hitResult?.item && vertices.includes(hitResult.item)) {
- if (isAltPressed) {
- selectedVertex = hitResult.item
- startPoint = null
- } else {
- startPoint = hitResult.item.position.clone()
- selectedVertex = null
- }
- } else {
- // Not clicking on a vertex
- if (isAltPressed) {
- // Start panning
- isPanning = true
- canvas2D.style.cursor = 'grab'
- lastPanPoint = event.point.clone()
- } else {
- // Start drawing
- selectedVertex = null
- startPoint = event.point.clone()
- }
- }
-}
-
-paper.view.onMouseDrag = (event) => {
- const isAltPressed = event.event.altKey
-
- if (isPanning && lastPanPoint) {
- canvas2D.style.cursor = 'grabbing'
- paper.view.translate(event.point.subtract(lastPanPoint))
- lastPanPoint = event.point.clone()
-
- }
-
- const dragDistance = selectedVertex
- ? selectedVertex.position.subtract(event.point).length
- : startPoint
- ? startPoint.subtract(event.point).length
- : 0
-
- if (dragDistance < MIN_DRAG_DISTANCE) return
-
- if (isAltPressed && selectedVertex) {
- isDraggingVertex = true
- cleanupPreview()
-
- const snappedPos = snapToGrid(event.point)
- selectedVertex.position = snappedPos
-
- selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
- path.segments[index].point = snappedPos
- if (path.data?.lengthText) {
- updateLengthText(path, path.data.lengthText)
- }
- })
- } else if (startPoint) {
- isDragging = true
- cleanupPreview()
-
- const snappedStart = snapToGrid(startPoint)
- const snappedEnd = snapToGrid(event.point)
-
- snapIndicator = new paper.Path.Circle({
- center: snappedEnd,
- radius: 4,
- fillColor: 'red',
- strokeColor: 'darkred',
- strokeWidth: 0.5,
- opacity: 0.8,
- })
-
- tempLine = new paper.Path.Line({
- from: snappedStart,
- to: snappedEnd,
- strokeColor: 'black',
- strokeWidth: LINE_STROKE_WIDTH,
- dashArray: [6, 4],
- opacity: 0.6,
- })
-
- tempText = new paper.PointText({
- point: snappedStart,
- content: '',
- fillColor: 'black',
- fontSize: 14,
- fontFamily: 'Arial',
- })
- updateLengthText(tempLine, tempText)
- }
-}
-
-paper.view.onMouseUp = (event) => {
- const isAltPressed = event.event.altKey // ✅ From mouse event
- cleanupPreview()
- if (isPanning) {
- isPanning = false
- lastPanPoint = null
- canvas2D.style.cursor = 'default'
- }
-
- if (isDraggingVertex && isAltPressed && selectedVertex) {
- const snappedPos = snapToGrid(selectedVertex.position)
- selectedVertex.position = snappedPos
- selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
- path.segments[index].point = snappedPos
- if (path.data?.lengthText) {
- updateLengthText(path, path.data.lengthText)
- }
- })
- } else if (isDragging && startPoint) {
- const snappedStart = snapToGrid(startPoint)
- const snappedEnd = snapToGrid(event.point)
-
- if (!snappedStart.equals(snappedEnd)) {
- let startVertex = vertices.find((v) => v.position.equals(snappedStart))
- let endVertex = vertices.find((v) => v.position.equals(snappedEnd))
-
- if (!startVertex) startVertex = createVertex(snappedStart)
- if (!endVertex) endVertex = createVertex(snappedEnd)
-
- const finalLine = new paper.Path.Line({
- from: snappedStart,
- to: snappedEnd,
- strokeColor: 'black',
- strokeWidth: LINE_STROKE_WIDTH,
- })
-
- const finalText = new paper.PointText({
- point: snappedStart,
- content: '',
- fillColor: 'black',
- fontSize: 14,
- fontFamily: 'Arial',
- })
- updateLengthText(finalLine, finalText)
- finalLine.data = { lengthText: finalText }
-
- startVertex.data.connectedPaths.push({ path: finalLine, index: 0 })
- endVertex.data.connectedPaths.push({ path: finalLine, index: 1 })
- }
- }
-
- startPoint = null
- selectedVertex = null
- isDragging = false
- isDraggingVertex = false
-}
diff --git a/src/js/main.js b/src/js/main.js
index b627e0a..d7d2008 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -1,41 +1,52 @@
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import paper from 'paper'
-import { createGrid } from './2d/floor2d.js'
+import { createGrid, setup2D } from './2d/core2d.js'
import { setupDxfUpload } from './2d/dxfLoader.js'
import { init3D, camera, renderer } from './3d/scene3d.js'
import { convertPathsTo3D } from './3d/pathsTo3d.js'
-// import parseDXF from 'dxf-parser'
-createGrid()
-setupDxfUpload()
-init3D()
+// ----------------------------------------------------
+// 1. Initialize 2D + 3D environments
+// ----------------------------------------------------
+setup2D() // Sets up Paper.js, grid, and context menu
+setupDxfUpload() // Enables DXF import
+init3D() // Sets up 3D scene
-// Switch between 2D and 3D modes
+// ----------------------------------------------------
+// 2. Switch between 2D and 3D modes
+// ----------------------------------------------------
const container2D = document.getElementById('container2D')
const container3D = document.getElementById('container3D')
const switchButton = document.getElementById('switchMode')
+
switchButton.addEventListener('click', () => {
if (container3D.style.display === 'none') {
+ // Switch to 3D
container2D.style.display = 'none'
container3D.style.display = 'block'
convertPathsTo3D()
switchButton.textContent = 'Switch to 2D'
} else {
+ // Switch to 2D
container2D.style.display = 'block'
container3D.style.display = 'none'
switchButton.textContent = 'Switch to 3D'
- // fixes when window is small that part of floorplan is cutted then turn to 3D the window is big again then turn to 2d the canvas is striched not resized
+
+ // Fix canvas stretch issue after resizing
paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight)
+ createGrid() // Recreate grid to fit new view bounds
}
})
-// Handle keyboard shortcuts toggle
+// ----------------------------------------------------
+// 3. Keyboard Shortcuts Toggle Panels
+// ----------------------------------------------------
function setupShortcutsToggle(headerId, contentId) {
const shortcutsHeader = document.getElementById(headerId)
const shortcutsContent = document.getElementById(contentId)
const shortcutsArrow = document.getElementsByClassName('shortcutsArrow')
- let shortcutsExpanded = false // Start expanded
+ let shortcutsExpanded = false // Start collapsed
shortcutsHeader.addEventListener('click', () => {
shortcutsExpanded = !shortcutsExpanded
@@ -53,10 +64,10 @@ function setupShortcutsToggle(headerId, contentId) {
setupShortcutsToggle('shortcutsHeader2D', 'shortcutsContent2D')
setupShortcutsToggle('shortcutsHeader3D', 'shortcutsContent3D')
-// Handle window resizing
+// ----------------------------------------------------
+// 4. Handle window resizing (3D responsiveness)
+// ----------------------------------------------------
window.addEventListener('resize', () => {
- // console.log('resizing...')
- // paper.view.viewSize = new paper.Size(window.innerWidth, window.innerHeight) # replaced by resize att in html
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
From 4913f37013f22a3dee274d8645791252e4345c78 Mon Sep 17 00:00:00 2001
From: Abdo-Eid <65284648+Abdo-Eid@users.noreply.github.com>
Date: Sun, 12 Oct 2025 12:33:37 +0300
Subject: [PATCH 4/4] UI: extend context menu with delete and clear actions -
Added 'Delete Line' and 'Clear Canvas' options to context menu - Improved
user workflow for canvas management
---
index.html | 1 -
src/js/2d/contextMenu2d.js | 23 ++++
src/js/2d/drawing2d.js | 213 +++++++++++++++++++++++++++++++++----
src/js/2d/dxfLoader.js | 6 +-
4 files changed, 222 insertions(+), 21 deletions(-)
diff --git a/index.html b/index.html
index 62c9d97..a32fbe0 100644
--- a/index.html
+++ b/index.html
@@ -33,7 +33,6 @@
- - Ctrl + Z: Undo last action
- Alt: Move tool allow dragging vertex
- Alt + grab: navigate the canvas
diff --git a/src/js/2d/contextMenu2d.js b/src/js/2d/contextMenu2d.js
index 7390b4d..ee34ea7 100644
--- a/src/js/2d/contextMenu2d.js
+++ b/src/js/2d/contextMenu2d.js
@@ -2,6 +2,7 @@
const ContextMenu = {
element: null,
options: [], // Store added options
+ lastClickClient: null, // store last right-click client coords
create(position) {
// Remove any existing context menu
@@ -12,6 +13,8 @@ const ContextMenu = {
this.element.className = 'context-menu'
this.element.style.top = `${position.y}px`
this.element.style.left = `${position.x}px`
+ // store last client coords for other modules to use
+ this.lastClickClient = { x: position.x, y: position.y }
// Add all stored options
this.options.forEach((option) => {
@@ -47,6 +50,26 @@ const ContextMenu = {
// Default option
ContextMenu.addOption('Close Menu', () => ContextMenu.remove())
+ContextMenu.addOption('Clear Canvas', () => {
+ const evt = new CustomEvent('datacenter:clearCanvas')
+ window.dispatchEvent(evt)
+})
+
+// Delete line option: will call into drawing logic using the last right-click position
+// Note: drawing2d.deleteLineNear expects a Paper.js Point; the context that opens
+// this menu (core2d) calls create with client coordinates. Consumers should convert
+// client->paper coordinates before calling deleteLineNear. To keep this file decoupled
+// we dispatch a custom event with the client coords and let drawing code listen for it.
+ContextMenu.addOption('Delete Line', () => {
+ const detail = {
+ clientX: ContextMenu.lastClickClient?.x,
+ clientY: ContextMenu.lastClickClient?.y,
+ }
+ // dispatch a custom event that drawing2d can listen to
+ const evt = new CustomEvent('datacenter:deleteLineAt', { detail })
+ window.dispatchEvent(evt)
+})
+
// ==========================================
// Safe initialization function
// ==========================================
diff --git a/src/js/2d/drawing2d.js b/src/js/2d/drawing2d.js
index 3d9f442..fff4bc6 100644
--- a/src/js/2d/drawing2d.js
+++ b/src/js/2d/drawing2d.js
@@ -1,9 +1,15 @@
-import { paper, LINE_STROKE_WIDTH, MIN_DRAG_DISTANCE, GRID_SPACING } from './core2d.js'
+import {
+ paper,
+ LINE_STROKE_WIDTH,
+ MIN_DRAG_DISTANCE,
+ GRID_SPACING,
+} from './core2d.js'
let vertices = []
/**
- * Snap a point to the grid
+ * Snap a point to the nearest grid intersection
+ * Ensures consistent alignment for drawing precision
*/
export function snapToGrid(point) {
return new paper.Point(
@@ -13,7 +19,8 @@ export function snapToGrid(point) {
}
/**
- * Create a red vertex circle at given point
+ * Create a visible vertex (red circle) at the given point
+ * Each vertex tracks its connected paths for later updates
*/
export function createVertex(point) {
const vertex = new paper.Path.Circle({
@@ -23,22 +30,157 @@ export function createVertex(point) {
strokeColor: 'darkred',
strokeWidth: 1,
})
- vertex.data = { connectedPaths: [] }
+ vertex.data = { connectedPaths: [] } // store connections for interactive updates
vertices.push(vertex)
vertex.bringToFront()
return vertex
}
+/**
+ * Accessor for all created vertices
+ */
export function getVertices() {
return vertices
}
+/**
+ * Remove all vertex objects from the canvas
+ * Used when resetting or clearing the drawing state
+ */
export function clearVertices() {
vertices.forEach((vertex) => vertex.remove())
vertices = []
}
+/**
+ * Find and delete a line near the given paper.Point within tolerance (pixels).
+ * Also cleans up connected vertices if they become orphaned.
+ * Returns true if a line was deleted, false otherwise.
+ */
+export function deleteLineNear(paperPoint, tolerance = 12) {
+ if (!paperPoint) return false
+
+ // Use Paper.js hit testing to get candidates near the point (optimized)
+ const hits = paper.project.hitTestAll(paperPoint, {
+ stroke: true,
+ tolerance,
+ fill: false,
+ segments: true,
+ })
+
+ if (!hits || hits.length === 0) return false
+
+ // Map to unique path items and filter to simple two-segment lines
+ const candidateItems = hits
+ .map((h) => h.item)
+ .filter((item) => item && item.segments && item.segments.length === 2)
+
+ if (candidateItems.length === 0) return false
+
+ // Pick the geometrically nearest candidate
+ let nearest = null
+ let nearestDist = Infinity
+ candidateItems.forEach((p) => {
+ try {
+ const nearestPt = p.getNearestPoint(paperPoint)
+ const d = nearestPt.getDistance(paperPoint)
+ if (d < nearestDist) {
+ nearestDist = d
+ nearest = p
+ }
+ } catch (err) {
+ // ignore malformed items
+ }
+ })
+
+ if (!nearest || nearestDist > tolerance) return false
+ // Remove associated length text if present
+ if (nearest.data?.lengthText) {
+ try {
+ nearest.data.lengthText.remove()
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ // For each endpoint, find matching vertex and remove the connection entry
+ const startPt = nearest.firstSegment.point
+ const endPt = nearest.lastSegment.point
+
+ const affectedVertices = vertices.filter(
+ (v) => v.position.equals(startPt) || v.position.equals(endPt)
+ )
+
+ affectedVertices.forEach((vertex) => {
+ vertex.data.connectedPaths = vertex.data.connectedPaths.filter(
+ (entry) => entry.path !== nearest
+ )
+ })
+
+ // Remove the path
+ nearest.remove()
+
+ // Remove orphaned vertices from canvas and vertices array
+ const remaining = []
+ vertices.forEach((v) => {
+ if (
+ !v.data ||
+ !v.data.connectedPaths ||
+ v.data.connectedPaths.length === 0
+ ) {
+ v.remove()
+ } else {
+ remaining.push(v)
+ }
+ })
+ vertices = remaining
+
+ return true
+}
+
+// Listen for context menu deletion events (client coordinates) and convert
+// to paper coordinates here to avoid introducing a dependency in the context menu.
+window.addEventListener('datacenter:deleteLineAt', (e) => {
+ const detail = e?.detail || {}
+ const clientX = detail.clientX
+ const clientY = detail.clientY
+ if (typeof clientX !== 'number' || typeof clientY !== 'number') return
+ // convert client to view/paper coordinates
+ const rect = document.getElementById('canvas2D')?.getBoundingClientRect()
+ if (!rect) return
+ const canvasPoint = new paper.Point(clientX - rect.left, clientY - rect.top)
+ deleteLineNear(canvasPoint)
+})
+
+/**
+ * Remove all user-drawn items from the canvas (except grid)
+ */
+export function clearCanvas() {
+ // Remove all items except the grid group (if present)
+ const all = paper.project.activeLayer.children.slice() // snapshot
+ all.forEach((item) => {
+ try {
+ if (item && item.name === 'grid') return // keep grid
+ // If the gridGroup is a group named 'grid', preserve it
+ if (item.parent && item.parent.name === 'grid') return
+ // Remove other items
+ item.remove()
+ } catch (e) {
+ // ignore
+ }
+ })
+
+ // Clear vertices array and any leftover visuals
+ clearVertices()
+}
+
+// Listen for clear canvas events from the context menu
+window.addEventListener('datacenter:clearCanvas', () => {
+ clearCanvas()
+})
+
+// Interaction state flags
let isPanning = false
let startPoint = null
let tempLine = null
@@ -49,15 +191,17 @@ let selectedVertex = null
let isDraggingVertex = false
/**
- * Update length text
+ * Update the position and value of a line's length text
+ * Converts length from pixels to meters based on canvas scale
*/
function updateLengthText(path, textItem) {
if (!textItem) return
const lengthInPixels = path.length
- const metersPerPixel = 10 / paper.view.bounds.width
+ const metersPerPixel = 10 / paper.view.bounds.width // dynamic scaling factor
const lengthInMeters = (lengthInPixels * metersPerPixel).toFixed(2)
+ // Position the text slightly offset from the midpoint
const direction = path.firstSegment.point.subtract(path.lastSegment.point)
if (direction.length === 0) return
const offset = direction.normalize().rotate(90).multiply(20)
@@ -66,6 +210,10 @@ function updateLengthText(path, textItem) {
textItem.point = path.getPointAt(path.length / 2).add(offset)
}
+/**
+ * Remove any temporary elements (preview line, text, snap indicator)
+ * Prevents clutter and overlapping visuals while dragging
+ */
function cleanupPreview() {
if (tempLine) {
tempLine.remove()
@@ -81,15 +229,19 @@ function cleanupPreview() {
}
}
-// Mouse event handlers
+/**
+ * Initialize interactive mouse event handling for drawing and editing
+ * - Left click: draw new lines or select vertices
+ * - Alt + drag: move vertices or pan view
+ */
export function setupMouseHandlers(canvas2D) {
paper.view.onMouseDown = (event) => {
isDragging = false
isDraggingVertex = false
- // ✅ Read Alt state from mouse event
const isAltPressed = event.event.altKey
+ // Detect if user clicked on an existing vertex
const hitResult = paper.project.hitTest(event.point, {
fill: true,
stroke: true,
@@ -99,21 +251,24 @@ export function setupMouseHandlers(canvas2D) {
})
if (hitResult?.item && getVertices().includes(hitResult.item)) {
+ // Clicking directly on a vertex
if (isAltPressed) {
+ // Alt + click: prepare for vertex drag
selectedVertex = hitResult.item
startPoint = null
} else {
+ // Regular click: prepare to start drawing from this vertex
startPoint = hitResult.item.position.clone()
selectedVertex = null
}
} else {
- // Not clicking on a vertex
+ // Clicking empty space
if (isAltPressed) {
- // Start panning
+ // Alt + click empty: start panning
isPanning = true
canvas2D.style.cursor = 'grab'
} else {
- // Start drawing
+ // Regular click: start drawing a new line
selectedVertex = null
startPoint = event.point.clone()
}
@@ -123,19 +278,22 @@ export function setupMouseHandlers(canvas2D) {
paper.view.onMouseDrag = (event) => {
const isAltPressed = event.event.altKey
+ // Handle view panning
if (isPanning) {
canvas2D.style.cursor = 'grabbing'
- paper.view.translate(event.delta.divide(2)) // the divition for stability
+ paper.view.translate(event.delta.divide(2)) // divide to reduce pan speed
}
+ // Compute drag distance for action threshold
const dragDistance = selectedVertex
? selectedVertex.position.subtract(event.point).length
: startPoint
? startPoint.subtract(event.point).length
: 0
- if (dragDistance < MIN_DRAG_DISTANCE) return
+ if (dragDistance < MIN_DRAG_DISTANCE) return // ignore micro movements
+ // Vertex movement mode (Alt + drag vertex)
if (isAltPressed && selectedVertex) {
isDraggingVertex = true
cleanupPreview()
@@ -143,19 +301,23 @@ export function setupMouseHandlers(canvas2D) {
const snappedPos = snapToGrid(event.point)
selectedVertex.position = snappedPos
+ // Update all lines connected to this vertex dynamically
selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
path.segments[index].point = snappedPos
if (path.data?.lengthText) {
updateLengthText(path, path.data.lengthText)
}
})
- } else if (startPoint) {
+ }
+ // Line drawing mode
+ else if (startPoint) {
isDragging = true
cleanupPreview()
const snappedStart = snapToGrid(startPoint)
const snappedEnd = snapToGrid(event.point)
+ // Show snap target indicator
snapIndicator = new paper.Path.Circle({
center: snappedEnd,
radius: 4,
@@ -165,6 +327,7 @@ export function setupMouseHandlers(canvas2D) {
opacity: 0.8,
})
+ // Draw preview dashed line
tempLine = new paper.Path.Line({
from: snappedStart,
to: snappedEnd,
@@ -174,6 +337,7 @@ export function setupMouseHandlers(canvas2D) {
opacity: 0.6,
})
+ // Show temporary measurement label
tempText = new paper.PointText({
point: snappedStart,
content: '',
@@ -186,33 +350,43 @@ export function setupMouseHandlers(canvas2D) {
}
paper.view.onMouseUp = (event) => {
- const isAltPressed = event.event.altKey // ✅ From mouse event
+ const isAltPressed = event.event.altKey
cleanupPreview()
+
+ // End panning
if (isPanning) {
isPanning = false
canvas2D.style.cursor = 'default'
}
+ // Finalize vertex drag
if (isDraggingVertex && isAltPressed && selectedVertex) {
const snappedPos = snapToGrid(selectedVertex.position)
selectedVertex.position = snappedPos
+
selectedVertex.data.connectedPaths.forEach(({ path, index }) => {
path.segments[index].point = snappedPos
if (path.data?.lengthText) {
updateLengthText(path, path.data.lengthText)
}
})
- } else if (isDragging && startPoint) {
+ }
+ // Finalize line creation
+ else if (isDragging && startPoint) {
const snappedStart = snapToGrid(startPoint)
const snappedEnd = snapToGrid(event.point)
if (!snappedStart.equals(snappedEnd)) {
- let startVertex = getVertices().find((v) => v.position.equals(snappedStart))
+ // Reuse existing vertices if they already exist on those points
+ let startVertex = getVertices().find((v) =>
+ v.position.equals(snappedStart)
+ )
let endVertex = getVertices().find((v) => v.position.equals(snappedEnd))
if (!startVertex) startVertex = createVertex(snappedStart)
if (!endVertex) endVertex = createVertex(snappedEnd)
+ // Create final line between vertices
const finalLine = new paper.Path.Line({
from: snappedStart,
to: snappedEnd,
@@ -220,6 +394,7 @@ export function setupMouseHandlers(canvas2D) {
strokeWidth: LINE_STROKE_WIDTH,
})
+ // Attach a permanent length label
const finalText = new paper.PointText({
point: snappedStart,
content: '',
@@ -230,14 +405,16 @@ export function setupMouseHandlers(canvas2D) {
updateLengthText(finalLine, finalText)
finalLine.data = { lengthText: finalText }
+ // Register mutual connections for dynamic updates
startVertex.data.connectedPaths.push({ path: finalLine, index: 0 })
endVertex.data.connectedPaths.push({ path: finalLine, index: 1 })
}
}
+ // Reset interaction state
startPoint = null
selectedVertex = null
isDragging = false
isDraggingVertex = false
}
-}
\ No newline at end of file
+}
diff --git a/src/js/2d/dxfLoader.js b/src/js/2d/dxfLoader.js
index a1f2ca2..0eec9d7 100644
--- a/src/js/2d/dxfLoader.js
+++ b/src/js/2d/dxfLoader.js
@@ -1,5 +1,6 @@
import paper from 'paper'
import { ensureGridVisible } from './core2d.js'
+import { clearCanvas } from './drawing2d.js'
/**
* loadDxfFile loads and parses a DXF file, scales its entities to fit the canvas,
@@ -28,8 +29,9 @@ export function loadDxfFile(file) {
console.log('Scale factor:', scale)
console.log('Offset:', offset)
- // Clear existing content
- paper.project.clear()
+ // Clear existing content but preserve the grid
+ // Use the clearCanvas helper which removes user drawings but keeps the grid group
+ clearCanvas()
dxfCircles.length = 0 // Clear previous circles
// Process entities with scaling and positioning