Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/entities/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions core/lib/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class Canvas {
doubleClick(button) {
switch (button) {
case 0: // left button
this.handleTextEditDoubleClick();
break;
case 1: // middle button
this.zoomExtents();
Expand All @@ -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
*/
Expand Down
16 changes: 14 additions & 2 deletions core/lib/commandLine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 + ' ';
Comment thread
dubstar-04 marked this conversation as resolved.
this.update();
return;
}

const activeCommand = DesignCore.Scene.inputManager.activeCommand;
const promptOption = DesignCore.Scene.inputManager.promptOption;

Expand All @@ -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();
}

/**
Expand Down
141 changes: 140 additions & 1 deletion core/lib/inputManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
}

/**
Expand All @@ -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();
}
}

/**
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {

Copilot AI Oct 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition this.editingTextIndex === undefined should use strict inequality (!== undefined) for consistency with the other checks, or consider checking for null as well since the property is initialized as undefined but could potentially be set to null. This same pattern appears in multiple methods (handleTextEditInput, confirmTextEdit, cancelTextEdit).

Copilot uses AI. Check for mistakes.
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();
}
}
39 changes: 39 additions & 0 deletions test/entities/text.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
};
Comment on lines +213 to +216

Copilot AI Oct 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly manipulating private properties (indicated by the _core naming convention) in tests creates tight coupling and fragility. Consider providing a proper mocking mechanism or test utility method in DesignCore for testing purposes.

Copilot uses AI. Check for mistakes.

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;
}
});