Skip to content

Commit 88b8a32

Browse files
committed
With this command the drag command and the zooming command work well togetther maintaing the stability of dragging after zooming in and out.
1 parent 563bf57 commit 88b8a32

File tree

2 files changed

+160
-144
lines changed

2 files changed

+160
-144
lines changed

src/gui/adapters/GUIAdapter.js

Lines changed: 142 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -9,152 +9,168 @@ import { CommandHistory } from "../commands/CommandHistory.js";
99
*
1010
* **Core Concepts**:
1111
* - **Event-Driven Execution**: UI interactions trigger commands rather than modifying state directly.
12-
* - **Dynamic Command Injection**: Commands are managed by `CommandFactory` and executed via `CommandHistory`.
12+
* - **Dynamic Command Injection**: Commands are managed by `GUICommandRegistry` and executed via `CommandHistory`.
1313
* - **Separation of Concerns**: The GUI delegates all logic to `CircuitService` via the command system.
1414
*
1515
* **Responsibilities**:
1616
* 1. **Initialization**:
1717
* - Renders the circuit and **binds UI controls dynamically**.
1818
* 2. **Command Execution**:
19-
* - Forwards user actions to `CommandFactory`, which then emits **commandExecuted** events.
19+
* - Retrieves commands from `GUICommandRegistry` and executes them.
2020
* 3. **Undo/Redo Support**:
2121
* - Ensures that every executed command is trackable via `CommandHistory`.
2222
*
23-
* **Example Workflow**:
24-
* 1. A user clicks the "Add Resistor" button.
25-
* 2. The `click` event triggers `executeCommand("addElement", "Resistor")`.
26-
* 3. `GUIAdapter` retrieves the command from `CommandFactory`.
27-
* 4. The command executes via `CommandHistory`.
28-
* 5. `CircuitService` adds the element and emits an `"update"` event.
29-
* 6. `GUIAdapter` listens for `"update"` and **re-renders the UI**.
30-
*
31-
* **Benefits**:
32-
* - **Decoupled UI & Business Logic**: The GUI does not directly modify the circuit.
33-
* - **Fully Extensible**: New commands can be added without modifying this class.
34-
* - **Undo/Redo Support**: Actions can be undone/redone via `CommandHistory`.
35-
*
3623
* @example
3724
* const guiAdapter = new GUIAdapter(canvas, circuitService, elementRegistry);
3825
* guiAdapter.initialize();
3926
*/
4027
export class GUIAdapter {
41-
/**
42-
* @param {HTMLCanvasElement} canvas - The canvas element for rendering the circuit.
43-
* @param {CircuitService} circuitService - The service managing circuit logic.
44-
* @param {Object} elementRegistry - The registry of circuit elements.
45-
* @param {RendererFactory} rendererFactory - The factory for creating element renderers.
46-
* @param {GUICommandRegistry} commandFactory - The factory for creating commands.
47-
*/
48-
constructor(canvas, circuitService, elementRegistry, rendererFactory, guiCommandRegistry) {
49-
this.canvas = canvas;
50-
this.circuitService = circuitService;
51-
this.elementRegistry = elementRegistry;
52-
this.circuitRenderer = new CircuitRenderer(canvas, circuitService, rendererFactory);
53-
this.guiCommandRegistry = guiCommandRegistry;
54-
55-
// this.commandFactory = new GUICommandFactory(circuitService, this.circuitRenderer, elementRegistry);
56-
this.commandHistory = new CommandHistory();
57-
}
28+
/**
29+
* @param {HTMLCanvasElement} canvas - The canvas element for rendering the circuit.
30+
* @param {CircuitService} circuitService - The service managing circuit logic.
31+
* @param {Object} elementRegistry - The registry of circuit elements.
32+
* @param {RendererFactory} rendererFactory - The factory for creating element renderers.
33+
* @param {GUICommandRegistry} guiCommandRegistry - The factory for creating commands.
34+
*/
35+
constructor(canvas, circuitService, elementRegistry, rendererFactory, guiCommandRegistry) {
36+
this.canvas = canvas;
37+
this.circuitService = circuitService;
38+
this.elementRegistry = elementRegistry;
39+
this.circuitRenderer = new CircuitRenderer(canvas, circuitService, rendererFactory);
40+
this.guiCommandRegistry = guiCommandRegistry;
41+
this.commandHistory = new CommandHistory();
42+
this.dragCommand = null;
43+
}
5844

59-
/**
60-
* Dynamically binds UI controls to their corresponding commands.
61-
* Instead of directly modifying circuit state, this now delegates execution to CommandFactory.
62-
*/
63-
bindUIControls() {
64-
console.log("🔍 Binding UI controls in GUIAdapter...");
65-
console.log("Commands available:", this.guiCommandRegistry.getTypes());
66-
67-
this.elementRegistry.getTypes().forEach((elementType) => {
68-
const buttonName = `add${elementType}`; // Format should match HTML IDs
69-
console.log(`🔍 Searching for button: ${buttonName}`);
70-
71-
const button = document.getElementById(buttonName);
72-
if (button) {
73-
console.log(`✅ Found button: ${button.id}, binding addElement command for ${elementType}`);
74-
75-
button.addEventListener("click", () => {
76-
console.log(`🛠 Executing addElement command for: ${elementType}`);
77-
78-
// Retrieve the correct command with the element type
79-
const command = this.guiCommandRegistry.get(
80-
"addElement",
81-
// this, // Pass GUIAdapter
82-
this.circuitService,
83-
this.circuitRenderer,
84-
this.elementRegistry,
85-
elementType
86-
);
87-
88-
if (command) {
89-
command.execute();
90-
console.log(`✅ Command 'addElement' executed for ${elementType}`);
91-
} else {
92-
console.warn(`⚠️ Command 'addElement' not found for ${elementType}`);
93-
}
94-
});
95-
} else {
96-
console.warn(`⚠️ Button for adding ${elementType} not found`);
97-
}
98-
});
99-
}
100-
/**
101-
* Executes a command by retrieving it from the CommandFactory and executing via CommandHistory.
102-
* @param {string} commandName - The name of the command to execute.
103-
* @param {...any} args - Arguments to pass to the command.
104-
*/
105-
executeCommand(commandName, ...args) {
106-
console.log(`Executing command: ${commandName} with args:`, args);
107-
108-
const command = this.commandFactory.get(commandName, ...args);
109-
if (command) {
110-
this.commandHistory.executeCommand(command, ...args);
111-
} else {
112-
console.warn(`Command "${commandName}" not found.`);
113-
}
114-
}
45+
/**
46+
* Dynamically binds UI controls to their corresponding commands.
47+
*/
48+
bindUIControls() {
49+
console.log("Binding UI controls in GUIAdapter...");
50+
console.log("Commands available:", this.guiCommandRegistry.getTypes());
11551

116-
/**
117-
* Initializes the GUI by rendering the circuit and binding UI controls.
118-
*/
119-
initialize() {
120-
this.circuitRenderer.render();
121-
this.bindUIControls();
122-
this.setupCanvasInteractions();
52+
this.elementRegistry.getTypes().forEach((elementType) => {
53+
const buttonName = `add${elementType}`;
54+
console.log(`Searching for button: ${buttonName}`);
12355

124-
// Listen for UI updates from CircuitService
125-
this.circuitService.on("update", () => this.circuitRenderer.render());
126-
}
56+
const button = document.getElementById(buttonName);
57+
if (button) {
58+
console.log(`Found button: ${button.id}, binding addElement command for ${elementType}`);
12759

128-
/**
129-
* Sets up canvas interactions for dragging elements.
130-
*/
131-
setupCanvasInteractions() {
132-
let dragCommand = null; // Cache the command to prevent infinite looping
133-
134-
this.canvas.addEventListener("mousedown", (event) => {
135-
const { offsetX, offsetY } = event;
136-
// Retrieve the drag command only ONCE when dragging starts
137-
dragCommand = this.guiCommandRegistry.get("dragElement", this.circuitService);
138-
139-
if (dragCommand) {
140-
dragCommand.start(offsetX, offsetY);
141-
} else {
142-
console.warn("⚠️ Drag command not found in registry");
143-
}
144-
});
60+
button.addEventListener("click", () => {
61+
console.log(`Executing addElement command for: ${elementType}`);
14562

146-
this.canvas.addEventListener("mousemove", (event) => {
147-
if (dragCommand) { // Only execute if a drag command exists
148-
const { offsetX, offsetY } = event;
149-
dragCommand.move(offsetX, offsetY);
150-
}
151-
});
63+
const command = this.guiCommandRegistry.get(
64+
"addElement",
65+
this.circuitService,
66+
this.circuitRenderer,
67+
this.elementRegistry,
68+
elementType
69+
);
15270

153-
this.canvas.addEventListener("mouseup", () => {
154-
if (dragCommand) {
155-
dragCommand.stop();
156-
dragCommand = null; // Reset command after stopping to prevent looping
157-
}
71+
if (command) {
72+
command.execute();
73+
console.log(`Command 'addElement' executed for ${elementType}`);
74+
} else {
75+
console.warn(`Command 'addElement' not found for ${elementType}`);
76+
}
15877
});
78+
} else {
79+
console.warn(`Button for adding ${elementType} not found`);
80+
}
81+
});
82+
}
83+
84+
/**
85+
* Executes a command by retrieving it from `GUICommandRegistry` and executing via `CommandHistory`.
86+
* @param {string} commandName - The name of the command to execute.
87+
* @param {...any} args - Arguments to pass to the command.
88+
*/
89+
executeCommand(commandName, ...args) {
90+
console.log(`Executing command: ${commandName} with args:`, args);
91+
92+
const command = this.guiCommandRegistry.get(commandName, ...args);
93+
if (command) {
94+
this.commandHistory.executeCommand(command, ...args);
95+
} else {
96+
console.warn(`Command "${commandName}" not found.`);
15997
}
98+
}
99+
100+
/**
101+
* Initializes the GUI by rendering the circuit and binding UI controls.
102+
*/
103+
initialize() {
104+
this.circuitRenderer.render();
105+
this.bindUIControls();
106+
this.setupCanvasInteractions();
107+
108+
// Listen for UI updates from CircuitService
109+
this.circuitService.on("update", () => this.circuitRenderer.render());
110+
}
111+
112+
/**
113+
* Sets up canvas interactions for dragging elements.
114+
*/
115+
setupCanvasInteractions() {
116+
this.canvas.addEventListener("wheel", (event) => {
117+
event.preventDefault();
118+
this.circuitRenderer.zoom(event);
119+
});
120+
121+
this.canvas.addEventListener("mousedown", (event) => {
122+
const { offsetX, offsetY } = this.getTransformedMousePosition(event);
123+
this.dragCommand = this.guiCommandRegistry.get("dragElement", this.circuitService);
124+
125+
if (this.dragCommand) {
126+
this.dragCommand.start(offsetX, offsetY);
127+
}
128+
});
129+
130+
this.canvas.addEventListener("mousemove", (event) => {
131+
if (this.dragCommand) {
132+
const { offsetX, offsetY } = this.getTransformedMousePosition(event);
133+
this.dragCommand.move(offsetX, offsetY);
134+
}
135+
});
136+
137+
this.canvas.addEventListener("mouseup", () => {
138+
if (this.dragCommand) {
139+
this.dragCommand.stop();
140+
this.dragCommand = null;
141+
}
142+
});
143+
144+
// Enable panning with middle mouse button
145+
this.canvas.addEventListener("mousedown", (event) => {
146+
if (event.button === 1) {
147+
this.canvas.style.cursor = "grabbing";
148+
this.panStartX = event.clientX - this.circuitRenderer.offsetX;
149+
this.panStartY = event.clientY - this.circuitRenderer.offsetY;
150+
}
151+
});
152+
153+
this.canvas.addEventListener("mousemove", (event) => {
154+
if (event.buttons === 4) {
155+
const newX = event.clientX - this.panStartX;
156+
const newY = event.clientY - this.panStartY;
157+
this.circuitRenderer.setPan(newX, newY);
158+
}
159+
});
160+
161+
this.canvas.addEventListener("mouseup", () => {
162+
this.canvas.style.cursor = "default";
163+
});
164+
}
165+
166+
/**
167+
* Adjusts mouse position based on zoom and pan.
168+
*/
169+
getTransformedMousePosition(event) {
170+
const rect = this.canvas.getBoundingClientRect();
171+
return {
172+
offsetX: (event.clientX - rect.left - this.circuitRenderer.offsetX) / this.circuitRenderer.scale,
173+
offsetY: (event.clientY - rect.top - this.circuitRenderer.offsetY) / this.circuitRenderer.scale,
174+
};
175+
}
160176
}

src/gui/commands/GUIDragElementCommand.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,28 @@ export class DragElementCommand extends GUICommand {
2323

2424
move(x, y) {
2525
if (this.draggedElement) {
26-
const scale = this.circuitService.zoomScale || 1; // ✅ Get current zoom level
27-
const dx = (x - this.offset.x) / scale; // ✅ Apply zoom correction
28-
const dy = (y - this.offset.y) / scale;
26+
const dx = (x - this.offset.x);
27+
const dy = (y - this.offset.y);
2928

30-
const firstNode = this.draggedElement.nodes[0];
31-
const deltaX = dx - firstNode.x;
32-
const deltaY = dy - firstNode.y;
29+
const firstNode = this.draggedElement.nodes[0];
30+
const deltaX = dx - firstNode.x;
31+
const deltaY = dy - firstNode.y;
3332

34-
this.draggedElement.nodes = this.draggedElement.nodes.map(
35-
(node) => new Position(node.x + deltaX, node.y + deltaY),
36-
);
33+
this.draggedElement.nodes = this.draggedElement.nodes.map((node) =>
34+
new Position(node.x + deltaX, node.y + deltaY)
35+
);
3736

38-
this.circuitService.emit("update", {
39-
type: "moveElement",
40-
element: this.draggedElement,
41-
});
37+
this.circuitService.emit("update", { type: "moveElement", element: this.draggedElement });
4238
}
43-
}
39+
}
4440

4541
stop() {
4642
this.draggedElement = null;
4743
}
4844

4945
// Helper method to check if a point is inside an element
5046
isInsideElement(x, y, element) {
51-
const auraSize = 10; // Expand clickable area beyond element size
47+
const auraSize = 10; // Expand clickable area beyond element size
5248

5349
if (element.nodes.length < 2) return false; // Must have at least two nodes
5450

@@ -57,10 +53,14 @@ export class DragElementCommand extends GUICommand {
5753
// Calculate distance of the point (x, y) from the element line
5854
const lineLength = Math.hypot(end.x - start.x, end.y - start.y);
5955
const distance =
60-
Math.abs((end.y - start.y) * x - (end.x - start.x) * y + end.x * start.y - end.y * start.x) / lineLength;
56+
Math.abs(
57+
(end.y - start.y) * x -
58+
(end.x - start.x) * y +
59+
end.x * start.y -
60+
end.y * start.x,
61+
) / lineLength;
6162

6263
// Allow clicks if within `auraSize` pixels of the element
6364
return distance <= auraSize;
64-
}
65-
65+
}
6666
}

0 commit comments

Comments
 (0)