From a18b2cde1bc277e80477cfec4c27431122b5870e Mon Sep 17 00:00:00 2001 From: Brett Parker Date: Mon, 27 Oct 2025 23:54:15 +0000 Subject: [PATCH 1/2] Add text edit text edit added via double click --- core/entities/text.js | 9 +++ core/lib/canvas.js | 21 ++++++ core/lib/commandLine.js | 16 ++++- core/lib/inputManager.js | 141 ++++++++++++++++++++++++++++++++++++- test/entities/text.test.js | 39 ++++++++++ 5 files changed, 223 insertions(+), 3 deletions(-) diff --git a/core/entities/text.js b/core/entities/text.js index 5e9b527f3..d13efae89 100644 --- a/core/entities/text.js +++ b/core/entities/text.js @@ -351,6 +351,15 @@ export class Text extends Entity { return rect; } + /** + * Check if this text is currently being edited + * @return {boolean} + */ + isBeingEdited() { + return DesignCore.Scene.inputManager.textEditing && + DesignCore.Scene.inputManager.editingTextItem === this; + } + /** * Draw the entity * @param {Object} ctx - context diff --git a/core/lib/canvas.js b/core/lib/canvas.js index c5f4248dd..a0d4a785d 100644 --- a/core/lib/canvas.js +++ b/core/lib/canvas.js @@ -109,6 +109,7 @@ export class Canvas { doubleClick(button) { switch (button) { case 0: // left button + this.handleTextEditDoubleClick(); break; case 1: // middle button this.zoomExtents(); @@ -118,6 +119,26 @@ export class Canvas { } }; + /** + * Handle double click for text editing + */ + handleTextEditDoubleClick() { + // Find the closest text entity to the mouse position + const mousePoint = DesignCore.Mouse.pointOnScene(); + const closestItemIndex = DesignCore.Scene.selectionManager.findClosestItem(mousePoint); + + if (closestItemIndex !== undefined) { + const item = DesignCore.Scene.items[closestItemIndex]; + + // Check if the item is a text entity + if (item.type === 'Text') { + // Start text editing mode + DesignCore.Scene.inputManager.startTextEdit(item, closestItemIndex); + return; + } + } + }; + /** * Pan the canvas */ diff --git a/core/lib/commandLine.js b/core/lib/commandLine.js index 5b777a27e..fdf12d227 100644 --- a/core/lib/commandLine.js +++ b/core/lib/commandLine.js @@ -75,7 +75,10 @@ export class CommandLine { this.updateCallbackFunction(this.cmdLine); } - if (DesignCore.Scene.inputManager.activeCommand !== undefined) { + // Handle text editing mode + if (DesignCore.Scene.inputManager.textEditing) { + DesignCore.Scene.inputManager.handleTextEditInput(this.command); + } else if (DesignCore.Scene.inputManager.activeCommand !== undefined) { // TODO: This should call a common function that is currently called mouseMove in the scene class DesignCore.Scene.tempItems = []; DesignCore.Scene.inputManager.activeCommand.preview(); @@ -168,6 +171,13 @@ export class CommandLine { * Handles presses of the space key */ spacePressed() { + // Handle text editing mode first + if (DesignCore.Scene.inputManager.textEditing) { + this.command = this.command + ' '; + this.update(); + return; + } + const activeCommand = DesignCore.Scene.inputManager.activeCommand; const promptOption = DesignCore.Scene.inputManager.promptOption; @@ -186,8 +196,10 @@ export class CommandLine { this.command = ''; } else { this.command = this.command.substring(0, this.command.length - 1); - this.update(); } + + // Always call update to keep command line in sync + this.update(); } /** diff --git a/core/lib/inputManager.js b/core/lib/inputManager.js index 34845b489..a92dc5319 100644 --- a/core/lib/inputManager.js +++ b/core/lib/inputManager.js @@ -172,6 +172,12 @@ export class InputManager { this.inputPoint = new Point(); this.snapping = new Snapping(); + + // Text editing state + this.textEditing = false; + this.editingTextItem = undefined; + this.editingTextIndex = undefined; + this.originalTextString = ''; } /** @@ -184,6 +190,11 @@ export class InputManager { // this.promptOption.reject('reject'); this.promptOption = undefined; DesignCore.Scene.reset(); + + // Exit text editing mode if active + if (this.textEditing) { + this.exitTextEdit(); + } } /** @@ -222,6 +233,14 @@ export class InputManager { * @param {any} input */ onCommand(input) { + // Handle text editing mode first + if (this.textEditing) { + // For text editing, when onCommand is called, it means Enter was pressed + // So we should confirm the edit + this.confirmTextEdit(); + return; + } + if (this.activeCommand !== undefined) { this.promptOption.respond(input); } else if (DesignCore.CommandManager.isCommandOrShortcut(input)) { @@ -234,6 +253,12 @@ export class InputManager { * Handle enter / return presses */ onEnterPressed() { + // Handle text editing mode first + if (this.textEditing) { + this.confirmTextEdit(); + return; + } + if (this.activeCommand !== undefined) { if (this.promptOption.types.includes(Input.Type.SELECTIONSET) && DesignCore.Scene.selectionManager.selectionSet.accepted !== true) { DesignCore.Scene.selectionManager.selectionSet.accepted = true; @@ -255,6 +280,12 @@ export class InputManager { * Handle escape presses to reset */ onEscapePressed() { + // Handle text editing mode first + if (this.textEditing) { + this.cancelTextEdit(); + return; + } + this.reset(); } @@ -316,7 +347,8 @@ export class InputManager { } // Determine if the mouse is over a scene item only if no snap point is available - if (snapped === undefined) { + // Skip selection/hover logic when in text editing mode + if (snapped === undefined && !this.textEditing) { if (this.activeCommand === undefined || this.activeCommand !== undefined && (this.promptOption.types.includes(Input.Type.SINGLESELECTION) || this.promptOption.types.includes(Input.Type.SELECTIONSET))) { const index = DesignCore.Scene.selectionManager.findClosestItem(DesignCore.Mouse.pointOnScene()); if (index !== undefined) { @@ -461,4 +493,111 @@ export class InputManager { return DesignCore.Scene.addItem(item.type, item, index); } } + + /** + * Start text editing mode + * @param {Object} textItem - the text item to edit + * @param {number} textIndex - index of the text item in scene.items + */ + startTextEdit(textItem, textIndex) { + this.textEditing = true; + this.editingTextItem = textItem; + this.editingTextIndex = textIndex; + this.originalTextString = textItem.string; + + // Set the command line content directly + DesignCore.CommandLine.command = textItem.string; + DesignCore.CommandLine.prompt = 'Edit text:'; + DesignCore.CommandLine.update(); + + // Clear any existing selection and temp items + DesignCore.Scene.selectionManager.reset(); + DesignCore.Scene.tempItems = []; + + // Mark scene as requiring save since we're editing + DesignCore.Scene.saveRequired(); + + // Request a repaint to show editing state + DesignCore.Canvas.requestPaint(); + } + + /** + * Exit text editing mode + */ + exitTextEdit() { + this.textEditing = false; + this.editingTextItem = undefined; + this.editingTextIndex = undefined; + this.originalTextString = ''; + + // Reset command line + DesignCore.CommandLine.resetPrompt(); + } + + /** + * Handle text editing input + * @param {string} input - the input text + */ + handleTextEditInput(input) { + if (!this.textEditing || !this.editingTextItem || this.editingTextIndex === undefined) { + return; + } + + // Get the current command line content + const currentText = DesignCore.CommandLine.command; + + // Update the scene item + const textItem = DesignCore.Scene.items[this.editingTextIndex]; + textItem.string = currentText; + + // Also update the editing item reference to keep them in sync + this.editingTextItem.string = currentText; + + // Update the command line display to ensure it's in sync + DesignCore.CommandLine.cmdLine = DesignCore.CommandLine.prompt + currentText; + if (DesignCore.CommandLine.updateCallbackFunction) { + DesignCore.CommandLine.updateCallbackFunction(DesignCore.CommandLine.cmdLine); + } + + // Force a repaint + DesignCore.Canvas.requestPaint(); + } + + /** + * Confirm text editing changes + */ + confirmTextEdit() { + if (!this.textEditing || !this.editingTextItem || this.editingTextIndex === undefined) { + return; + } + + // The scene item is already updated during typing, so we just need to exit editing mode + // Mark scene as requiring save + DesignCore.Scene.saveRequired(); + + // Exit text editing mode + this.exitTextEdit(); + + // Request a repaint + DesignCore.Canvas.requestPaint(); + } + + /** + * Cancel text editing and restore original text + */ + cancelTextEdit() { + if (!this.textEditing || !this.editingTextItem || this.editingTextIndex === undefined) { + return; + } + + // Restore original text + this.editingTextItem.string = this.originalTextString; + DesignCore.Scene.items[this.editingTextIndex].string = this.originalTextString; + + // Exit text editing mode + this.exitTextEdit(); + + // Request a repaint + DesignCore.Canvas.requestPaint(); + } } diff --git a/test/entities/text.test.js b/test/entities/text.test.js index 2e6543aa2..b4593fe06 100644 --- a/test/entities/text.test.js +++ b/test/entities/text.test.js @@ -1,5 +1,6 @@ import {Point} from '../../core/entities/point'; import {Text} from '../../core/entities/text'; +import {DesignCore} from '../../core/designCore.js'; import {File} from '../test-helpers/test-helpers.js'; @@ -193,3 +194,41 @@ AcDbText newText.dxf(file); expect(file.contents).toEqual(dxfString); }); + +test('Test Text.isBeingEdited', () => { + const text = new Text({points: [new Point(100, 100)], string: 'Test Text'}); + + // Mock DesignCore.Scene.inputManager + const mockInputManager = { + textEditing: false, + editingTextItem: null + }; + + // Mock DesignCore.Scene + const mockScene = { + inputManager: mockInputManager + }; + + // Mock DesignCore with proper structure including Core + const originalCore = DesignCore._core; + DesignCore._core = { + scene: mockScene + }; + + try { + // Test when not being edited + expect(text.isBeingEdited()).toBe(false); + + // Test when text editing is active but different item + mockInputManager.textEditing = true; + mockInputManager.editingTextItem = new Text({points: [new Point(200, 200)]}); + expect(text.isBeingEdited()).toBe(false); + + // Test when this text is being edited + mockInputManager.editingTextItem = text; + expect(text.isBeingEdited()).toBe(true); + } finally { + // Restore original DesignCore + DesignCore._core = originalCore; + } +}); From ce6b7b9fdd74f2bcbb92aaca082b6ca9d43fb398 Mon Sep 17 00:00:00 2001 From: Brett Parker Date: Tue, 28 Oct 2025 08:56:28 +0000 Subject: [PATCH 2/2] Fixed linting --- test/entities/text.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/entities/text.test.js b/test/entities/text.test.js index b4593fe06..52029c90c 100644 --- a/test/entities/text.test.js +++ b/test/entities/text.test.js @@ -197,33 +197,33 @@ AcDbText test('Test Text.isBeingEdited', () => { const text = new Text({points: [new Point(100, 100)], string: 'Test Text'}); - + // Mock DesignCore.Scene.inputManager const mockInputManager = { textEditing: false, - editingTextItem: null + editingTextItem: null, }; - + // Mock DesignCore.Scene const mockScene = { - inputManager: mockInputManager + inputManager: mockInputManager, }; - + // Mock DesignCore with proper structure including Core const originalCore = DesignCore._core; DesignCore._core = { - scene: mockScene + scene: mockScene, }; - + try { // Test when not being edited expect(text.isBeingEdited()).toBe(false); - + // Test when text editing is active but different item mockInputManager.textEditing = true; mockInputManager.editingTextItem = new Text({points: [new Point(200, 200)]}); expect(text.isBeingEdited()).toBe(false); - + // Test when this text is being edited mockInputManager.editingTextItem = text; expect(text.isBeingEdited()).toBe(true);