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..a32fbe0 100644
--- a/index.html
+++ b/index.html
@@ -21,22 +21,37 @@
+
Keyboard Shortcuts ▼
-
Ctrl + Z: Undo last action
+
Alt: Move tool allow dragging vertex
+
Alt + grab: navigate the canvas
-
+
+
Keyboard Shortcuts ▼
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..ee34ea7
--- /dev/null
+++ b/src/js/2d/contextMenu2d.js
@@ -0,0 +1,98 @@
+// Context menu object
+const ContextMenu = {
+ element: null,
+ options: [], // Store added options
+ lastClickClient: null, // store last right-click client coords
+
+ 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`
+ // 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) => {
+ 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())
+
+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
+// ==========================================
+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..fff4bc6
--- /dev/null
+++ b/src/js/2d/drawing2d.js
@@ -0,0 +1,420 @@
+import {
+ paper,
+ LINE_STROKE_WIDTH,
+ MIN_DRAG_DISTANCE,
+ GRID_SPACING,
+} from './core2d.js'
+
+let vertices = []
+
+/**
+ * Snap a point to the nearest grid intersection
+ * Ensures consistent alignment for drawing precision
+ */
+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 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({
+ center: point,
+ radius: 6,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 1,
+ })
+ 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
+let tempText = null
+let snapIndicator = null
+let isDragging = false
+let selectedVertex = null
+let isDraggingVertex = false
+
+/**
+ * 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 // 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)
+
+ textItem.content = `${lengthInMeters} m`
+ 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()
+ tempLine = null
+ }
+ if (tempText) {
+ tempText.remove()
+ tempText = null
+ }
+ if (snapIndicator) {
+ snapIndicator.remove()
+ snapIndicator = null
+ }
+}
+
+/**
+ * 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
+
+ const isAltPressed = event.event.altKey
+
+ // Detect if user clicked on an existing vertex
+ 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)) {
+ // 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 {
+ // Clicking empty space
+ if (isAltPressed) {
+ // Alt + click empty: start panning
+ isPanning = true
+ canvas2D.style.cursor = 'grab'
+ } else {
+ // Regular click: start drawing a new line
+ selectedVertex = null
+ startPoint = event.point.clone()
+ }
+ }
+ }
+
+ 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)) // 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 // ignore micro movements
+
+ // Vertex movement mode (Alt + drag vertex)
+ if (isAltPressed && selectedVertex) {
+ isDraggingVertex = true
+ cleanupPreview()
+
+ 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)
+ }
+ })
+ }
+ // 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,
+ fillColor: 'red',
+ strokeColor: 'darkred',
+ strokeWidth: 0.5,
+ opacity: 0.8,
+ })
+
+ // Draw preview dashed line
+ tempLine = new paper.Path.Line({
+ from: snappedStart,
+ to: snappedEnd,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ dashArray: [6, 4],
+ opacity: 0.6,
+ })
+
+ // Show temporary measurement label
+ 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
+ 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)
+ }
+ })
+ }
+ // Finalize line creation
+ else if (isDragging && startPoint) {
+ const snappedStart = snapToGrid(startPoint)
+ const snappedEnd = snapToGrid(event.point)
+
+ if (!snappedStart.equals(snappedEnd)) {
+ // 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,
+ strokeColor: 'black',
+ strokeWidth: LINE_STROKE_WIDTH,
+ })
+
+ // Attach a permanent length label
+ const finalText = new paper.PointText({
+ point: snappedStart,
+ content: '',
+ fillColor: 'black',
+ fontSize: 14,
+ fontFamily: 'Arial',
+ })
+ 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
+ }
+}
diff --git a/src/js/2d/dxfLoader.js b/src/js/2d/dxfLoader.js
index 5d2f6a4..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 './floor2d.js'
+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
diff --git a/src/js/2d/floor2d.js b/src/js/2d/floor2d.js
deleted file mode 100644
index 27a7292..0000000
--- a/src/js/2d/floor2d.js
+++ /dev/null
@@ -1,453 +0,0 @@
-import paper from 'paper'
-
-const canvas2D = document.getElementById('canvas2D')
-export { canvas2D }
-
-const GRID_SPACING = 30
-paper.setup(canvas2D)
-
-let actionHistory = []
-let MAX_HISTORY_LENGTH = 50
-
-/**
- * createGrid creates a 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 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)
- line.strokeColor = color
- line.strokeWidth = 1
- line.opacity = 0.7 // More visible
- 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)
- line.strokeColor = color
- line.strokeWidth = 1
- line.opacity = 0.7 // More visible
- gridGroup.addChild(line)
- }
-
- gridGroup.sendToBack()
- gridGroup.visible = true
- gridGroup.opacity = 1
- console.log('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() {
- /**
- * @type {paper.Item | null}
- */
- let grid = paper.project.getItem({ name: 'grid' })
- if (!grid) {
- console.log('No grid found, creating new one')
- grid = createGrid()
- } else {
- grid.visible = true
- grid.opacity = 1
- grid.sendToBack()
- console.log('Grid made visible')
- }
- 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
- }
- })
-
- // 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,
- })
-
- if (hitResult && hitResult.item.className === 'Path') {
- selectedLine = hitResult.item
- createContextMenu(event.event) // Pass the mouse event to position the menu
- return
- }
- }
-
- // 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)
- }
- 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,
- })
- }
-}
-
-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
-
- if (!start) start = createVertex(startVertex)
- if (!end) end = createVertex(endVertex)
-
- 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()
- }
-})
-
-canvas2D.addEventListener('contextmenu', (e) => {
- e.preventDefault()
-})
diff --git a/src/js/main.js b/src/js/main.js
index 416c2a2..d7d2008 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -1,39 +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'
+
+ // 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
@@ -51,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)
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