11/**
22 * @class GUIAdapter
33 * @description
4- * The `GUIAdapter` serves as the bridge between the user interface (UI) and the underlying application logic.
5- * Instead of directly modifying the circuit, it retrieves and executes commands dynamically .
4+ * The `GUIAdapter` bridges the UI and application logic. It transforms user input
5+ * into domain operations via command patterns and ensures state changes are traceable .
66 *
77 * **Core Concepts**:
8- * - **Event-Driven Execution**: UI interactions trigger commands rather than modifying state directly .
9- * - **Dynamic Command Injection**: Commands are managed by `GUICommandRegistry` and executed via `CommandHistory `.
10- * - **Separation of Concerns **: The GUI delegates all logic to `CircuitService` via the command system .
8+ * - **Event-Driven Execution**: User actions trigger command objects .
9+ * - **Command Injection**: Commands are dynamically created via `GUICommandRegistry `.
10+ * - **Undo/Redo **: Commands are managed and reversible via `CommandHistory` .
1111 *
1212 * **Responsibilities**:
13- * 1. **Initialization**:
14- * - Renders the circuit and **binds UI htmlelment controls/buttons dynamically**.
15- * 2. **Command Execution**:
16- * - Retrieves commands from `GUICommandRegistry` and executes them.
17- * 3. **Undo/Redo Support**:
18- * - Ensures that every executed command is trackable via `CommandHistory`.
19- *
20- * @example
21- * const guiAdapter = new GUIAdapter(canvas, circuitService, elementRegistry);
22- * guiAdapter.initialize();
13+ * 1. Render circuit elements via `CircuitRenderer`.
14+ * 2. Bind UI controls and buttons to domain commands.
15+ * 3. Coordinate undo/redo functionality.
2316 */
2417import { CircuitRenderer } from "../renderers/CircuitRenderer.js" ;
2518import { CommandHistory } from "../commands/CommandHistory.js" ;
2619
2720export class GUIAdapter {
28- constructor ( controls , canvas , circuitService , elementRegistry , rendererFactory , guiCommandRegistry ) {
21+ /**
22+ * @constructor
23+ * @param {HTMLElement } controls - DOM container for command buttons.
24+ * @param {HTMLCanvasElement } canvas - Canvas for circuit rendering.
25+ * @param {CircuitService } circuitService - Core domain service.
26+ * @param {ElementRegistry } elementRegistry - Registry of circuit components.
27+ * @param {RendererFactory } rendererFactory - Factory for creating renderers.
28+ * @param {GUICommandRegistry } guiCommandRegistry - Registry for GUI commands.
29+ */
30+ constructor (
31+ controls ,
32+ canvas ,
33+ circuitService ,
34+ elementRegistry ,
35+ rendererFactory ,
36+ guiCommandRegistry ,
37+ ) {
2938 this . controls = controls ;
3039 this . canvas = canvas ;
3140 this . circuitService = circuitService ;
3241 this . elementRegistry = elementRegistry ;
33- this . circuitRenderer = new CircuitRenderer ( canvas , circuitService , rendererFactory ) ;
42+ this . circuitRenderer = new CircuitRenderer (
43+ canvas ,
44+ circuitService ,
45+ rendererFactory ,
46+ ) ;
3447 this . guiCommandRegistry = guiCommandRegistry ;
3548 this . commandHistory = new CommandHistory ( ) ;
36- this . dragCommand = null ;
49+ this . activeCommand = null ;
3750 this . hasDragged = false ;
3851 this . mouseDownPos = { x : 0 , y : 0 } ;
39- this . activeCommand = null ;
4052 }
4153
54+ /**
55+ * Initializes the GUI, binds controls, sets up canvas listeners, and renders.
56+ */
4257 initialize ( ) {
43- console . log ( "GUIAdapter initialized" ) ;
4458 this . circuitRenderer . render ( ) ;
4559 this . bindUIControls ( ) ;
4660 this . setupCanvasInteractions ( ) ;
47-
4861 this . circuitService . on ( "update" , ( ) => this . circuitRenderer . render ( ) ) ;
4962 }
5063
64+ /**
65+ * Executes a named command through the command registry and stores it in history.
66+ * @param {string } commandName - Identifier for the command.
67+ * @param {...any } args - Arguments passed to the command.
68+ */
5169 executeCommand ( commandName , ...args ) {
5270 const command = this . guiCommandRegistry . get ( commandName , ...args ) ;
5371 if ( command ) {
@@ -57,29 +75,28 @@ export class GUIAdapter {
5775 }
5876 }
5977
78+ /**
79+ * Binds UI buttons to element creation and undo/redo commands.
80+ */
6081 bindUIControls ( ) {
6182 this . elementRegistry . getTypes ( ) . forEach ( ( elementType ) => {
6283 const buttonName = `add${ elementType } ` ;
63- console . log ( `Searching for button: ${ buttonName } ` ) ;
64-
6584 const oldButton = this . controls . querySelector ( `#${ buttonName } ` ) ;
6685 if ( oldButton ) {
6786 const button = oldButton . cloneNode ( true ) ;
6887 oldButton . replaceWith ( button ) ;
6988
70- console . log ( `Found button: ${ button . id } , binding addElement command for ${ elementType } ` ) ;
7189 button . addEventListener ( "click" , ( ) => {
7290 const command = this . guiCommandRegistry . get (
7391 "addElement" ,
7492 this . circuitService ,
7593 this . circuitRenderer ,
7694 this . elementRegistry ,
77- elementType
95+ elementType ,
7896 ) ;
7997
8098 if ( command ) {
8199 this . commandHistory . executeCommand ( command , this . circuitService ) ;
82- console . log ( `Command 'addElement' executed for ${ elementType } ` ) ;
83100 } else {
84101 console . warn ( `Command 'addElement' not found for ${ elementType } ` ) ;
85102 }
@@ -89,29 +106,36 @@ export class GUIAdapter {
89106 }
90107 } ) ;
91108
92- const undoButton = this . controls . querySelector ( "#undoButton" ) ;
93- if ( undoButton ) {
94- undoButton . replaceWith ( undoButton . cloneNode ( true ) ) ;
95- this . controls . querySelector ( "#undoButton" ) . addEventListener ( "click" , ( ) => {
96- this . commandHistory . undo ( this . circuitService ) ;
97- this . circuitRenderer . render ( ) ;
98- } ) ;
99- } else {
100- console . warn ( "Undo button not found" ) ;
101- }
109+ this . bindUndoRedo ( "#undoButton" , ( ) =>
110+ this . commandHistory . undo ( this . circuitService ) ,
111+ ) ;
112+ this . bindUndoRedo ( "#redoButton" , ( ) =>
113+ this . commandHistory . redo ( this . circuitService ) ,
114+ ) ;
115+ }
102116
103- const redoButton = this . controls . querySelector ( "#redoButton" ) ;
104- if ( redoButton ) {
105- redoButton . replaceWith ( redoButton . cloneNode ( true ) ) ;
106- this . controls . querySelector ( "#redoButton" ) . addEventListener ( "click" , ( ) => {
107- this . commandHistory . redo ( this . circuitService ) ;
117+ /**
118+ * Helper for binding undo/redo buttons.
119+ * @param {string } selector - DOM selector for the button.
120+ * @param {Function } action - Function to execute on click.
121+ */
122+ bindUndoRedo ( selector , action ) {
123+ const button = this . controls . querySelector ( selector ) ;
124+ if ( button ) {
125+ const clone = button . cloneNode ( true ) ;
126+ button . replaceWith ( clone ) ;
127+ clone . addEventListener ( "click" , ( ) => {
128+ action ( ) ;
108129 this . circuitRenderer . render ( ) ;
109130 } ) ;
110131 } else {
111- console . warn ( "Redo button not found" ) ;
132+ console . warn ( ` ${ selector } not found` ) ;
112133 }
113134 }
114135
136+ /**
137+ * Sets up mouse events for interaction on the canvas: zoom, drag, draw.
138+ */
115139 setupCanvasInteractions ( ) {
116140 this . canvas . addEventListener ( "wheel" , ( event ) => {
117141 event . preventDefault ( ) ;
@@ -128,17 +152,20 @@ export class GUIAdapter {
128152
129153 if ( event . button === 0 ) {
130154 const { offsetX, offsetY } = this . getTransformedMousePosition ( event ) ;
131-
132155 const element = this . findElementAt ( offsetX , offsetY ) ;
133- if ( element ) {
134- this . activeCommand = this . guiCommandRegistry . get ( "dragElement" , this . circuitService ) ;
135- } else {
136- this . activeCommand = this . guiCommandRegistry . get ( "drawWire" , this . circuitService , this . elementRegistry ) ;
137- }
156+
157+ this . activeCommand = element
158+ ? this . guiCommandRegistry . get ( "dragElement" , this . circuitService )
159+ : this . guiCommandRegistry . get (
160+ "drawWire" ,
161+ this . circuitService ,
162+ this . elementRegistry ,
163+ ) ;
138164
139165 if ( this . activeCommand ) {
140- this . activeCommand . beforeSnapshot = this . circuitService . exportState ( ) ;
166+ const before = this . circuitService . exportState ( ) ; // snapshot before changes
141167 this . activeCommand . start ( offsetX , offsetY ) ;
168+ this . activeCommand . beforeSnapshot = before ;
142169 }
143170
144171 this . hasDragged = false ;
@@ -149,14 +176,11 @@ export class GUIAdapter {
149176 this . canvas . addEventListener ( "mousemove" , ( event ) => {
150177 if ( this . activeCommand ) {
151178 const { offsetX, offsetY } = this . getTransformedMousePosition ( event ) ;
152-
153179 const dx = offsetX - this . mouseDownPos . x ;
154180 const dy = offsetY - this . mouseDownPos . y ;
155- const distance = Math . sqrt ( dx * dx + dy * dy ) ;
156- if ( distance > 2 ) {
181+ if ( Math . sqrt ( dx * dx + dy * dy ) > 2 ) {
157182 this . hasDragged = true ;
158183 }
159-
160184 this . activeCommand . move ( offsetX , offsetY ) ;
161185 }
162186 } ) ;
@@ -168,63 +192,116 @@ export class GUIAdapter {
168192 }
169193
170194 if ( this . activeCommand ) {
195+ // Make sure snapshot was taken before any state changes
196+ const before = this . activeCommand . beforeSnapshot ;
197+
171198 if ( ! this . hasDragged && this . activeCommand . cancel ) {
172199 this . activeCommand . cancel ( ) ;
173200 } else {
174201 this . activeCommand . stop ( ) ;
175- const before = this . activeCommand . beforeSnapshot ;
176202 const after = this . circuitService . exportState ( ) ;
177203
178- const snapshotCommand = {
179- execute : ( ) => this . circuitService . importState ( after ) ,
180- undo : ( ) => this . circuitService . importState ( before ) ,
181- } ;
204+ console . log ( "[GUI Adapter] State before:" , JSON . stringify ( before , null , 2 ) ) ;
205+ console . log ( "[GUI Adapter] State after :" , JSON . stringify ( after , null , 2 ) ) ;
206+
207+ if ( this . hasStateChanged ( before , after ) ) {
208+ console . log ( "[GUIAdapter] State changed — pushing to history." ) ;
209+ this . circuitService . importState ( before ) ;
182210
183- this . commandHistory . executeCommand ( snapshotCommand , this . circuitService ) ;
211+ const snapshotCommand = {
212+ execute : ( ) => this . circuitService . importState ( after ) ,
213+ undo : ( ) => this . circuitService . importState ( before ) ,
214+ } ;
215+
216+ this . commandHistory . executeCommand (
217+ snapshotCommand ,
218+ this . circuitService ,
219+ ) ;
220+ } else {
221+ console . warn (
222+ "[GUIAdapter] No state change — skipping history push." ,
223+ ) ;
224+ }
184225 }
185226
186227 this . activeCommand = null ;
187228 }
188229 } ) ;
189230 }
190231
232+ /**
233+ * Converts screen coordinates to world coordinates.
234+ * @param {MouseEvent } event
235+ * @returns {{offsetX: number, offsetY: number} }
236+ */
191237 getTransformedMousePosition ( event ) {
192238 const rect = this . canvas . getBoundingClientRect ( ) ;
193239 return {
194- offsetX : ( event . clientX - rect . left - this . circuitRenderer . offsetX ) / this . circuitRenderer . scale ,
195- offsetY : ( event . clientY - rect . top - this . circuitRenderer . offsetY ) / this . circuitRenderer . scale ,
240+ offsetX :
241+ ( event . clientX - rect . left - this . circuitRenderer . offsetX ) /
242+ this . circuitRenderer . scale ,
243+ offsetY :
244+ ( event . clientY - rect . top - this . circuitRenderer . offsetY ) /
245+ this . circuitRenderer . scale ,
196246 } ;
197247 }
198248
249+ /**
250+ * Finds the first element at the given world coordinates.
251+ * @param {number } worldX
252+ * @param {number } worldY
253+ * @returns {Element|null }
254+ */
199255 findElementAt ( worldX , worldY ) {
200- for ( const element of this . circuitService . getElements ( ) ) {
201- if ( this . isInsideElement ( worldX , worldY , element ) ) {
202- return element ;
203- }
204- }
205- return null ;
256+ return (
257+ this . circuitService
258+ . getElements ( )
259+ . find ( ( el ) => this . isInsideElement ( worldX , worldY , el ) ) || null
260+ ) ;
206261 }
207262
263+ /**
264+ * Determines if a point is inside or near a circuit element.
265+ * @param {number } x
266+ * @param {number } y
267+ * @param {Element } element
268+ * @returns {boolean }
269+ */
208270 isInsideElement ( x , y , element ) {
209271 if ( element . nodes . length < 2 ) return false ;
210-
211272 const aura = 10 ;
212273 const [ start , end ] = element . nodes ;
213274 const dx = end . x - start . x ;
214275 const dy = end . y - start . y ;
215276 const length = Math . hypot ( dx , dy ) ;
216- if ( length < 1e-6 ) {
217- return Math . hypot ( x - start . x , y - start . y ) <= aura ;
218- }
219- const distance = Math . abs ( dy * x - dx * y + end . x * start . y - end . y * start . x ) / length ;
277+ if ( length < 1e-6 ) return Math . hypot ( x - start . x , y - start . y ) <= aura ;
278+ const distance =
279+ Math . abs ( dy * x - dx * y + end . x * start . y - end . y * start . x ) / length ;
220280 if ( distance > aura ) return false ;
221-
222281 const minX = Math . min ( start . x , end . x ) - aura ;
223282 const maxX = Math . max ( start . x , end . x ) + aura ;
224283 const minY = Math . min ( start . y , end . y ) - aura ;
225284 const maxY = Math . max ( start . y , end . y ) + aura ;
226- if ( x < minX || x > maxX || y < minY || y > maxY ) return false ;
285+ return ! ( x < minX || x > maxX || y < minY || y > maxY ) ;
286+ }
227287
228- return true ;
288+ /**
289+ * Compares two circuit snapshots to determine if a meaningful change occurred.
290+ * @param {Object } before
291+ * @param {Object } after
292+ * @returns {boolean }
293+ */
294+ hasStateChanged ( before , after ) {
295+ before = JSON . parse ( before ) ;
296+ after = JSON . parse ( after ) ;
297+ if ( ! before || ! after ) return true ;
298+ if ( before . elements . length !== after . elements . length ) return true ;
299+ for ( let i = 0 ; i < before . elements . length ; i ++ ) {
300+ const a = before . elements [ i ] ;
301+ const b = after . elements [ i ] ;
302+ if ( a . id !== b . id || a . type !== b . type ) return true ;
303+ if ( JSON . stringify ( a . nodes ) !== JSON . stringify ( b . nodes ) ) return true ;
304+ }
305+ return false ;
229306 }
230307}
0 commit comments