gui/adapters/GUIAdapter.js

/**
 * @module GUI/Adapters
 * @description
 * 🎨 **GUI Layer - Application Adapters**
 *
 * Adapters that bridge the gap between user interface interactions and application
 * services, following the Hexagonal Architecture pattern.
 */

/**
 * @module GUIAdapter
 * @description
 * Bridges UI and application logic.
 *
 * UI adapters (menubar, keyboard, mouse) emit **semantic action ids** (from YAML/JSON config).
 * GUIAdapter maps those ids to domain commands (via GUICommandRegistry + CommandHistory)
 * or to view-only renderer operations. State changes emit "update" → Renderer re-renders.
 *
 */

import { CircuitRenderer } from "../renderers/CircuitRenderer.js";
import { CommandHistory } from "../commands/CommandHistory.js";
import { ACTIONS, KEYMAP } from "../../config/menu.bindings.js";
import { PropertyPanel } from "../property_panel/PropertyPanel.js";
import { Logger } from "../../utils/Logger.js";
import { throttle } from "../../utils/PerformanceUtils.js";
import { GRID_CONFIG } from "../../config/gridConfig.js";


/**
 * @class GUIAdapter
 * @classdesc
 * **🔧 Primary GUI Extension Point**
 *
 * Translates user intents to domain operations (commands) or view operations.
 * This is the main class developers will work with to customize GUI behavior,
 * add new menu actions, or integrate custom interaction patterns.
 *
 * **Core Concepts**
 * - Event-driven execution (actions come from config-driven UI).
 * - Command injection via GUICommandRegistry.
 * - Undo/Redo via CommandHistory.
 *
 * **Extension Points for Developers:**
 * - Add new menu actions via action configuration
 * - Register custom commands in GUICommandRegistry
 * - Override or extend interaction handlers
 * - Customize keyboard shortcuts and bindings
 *
 * **Responsibilities**
 * 1) **Event Translation**: Convert UI events (mouse, keyboard, menu) to semantic actions
 * 2) **Command Orchestration**: Route actions to appropriate commands via GUICommandRegistry
 * 3) **State Management**: Manage interaction states (wire mode, element placement, selection)
 * 4) **Canvas Coordination**: Integrate with CircuitRenderer for visual updates
 * 5) **History Management**: Coordinate undo/redo operations via CommandHistory
 * 6) **Property Panel Integration**: Manage property panel interactions
 *
 * **Key Extension Patterns:**
 *
 * @example
 * // Adding custom action handlers
 * const guiAdapter = new GUIAdapter(canvas, circuitService, elementRegistry,
 *                                   rendererFactory, guiCommandRegistry);
 *
 * // Custom actions are defined in menu.config.yaml and handled automatically
 * guiAdapter.handleAction('custom.myAction');
 *
 * // Custom commands should be registered in GUICommandRegistry
 * guiCommandRegistry.register('myCommand', (services) =>
 *   new MyCustomCommand(services.circuitService, services.circuitRenderer));
 *
 * @example
 * // Extending keyboard shortcuts (done in configuration)
 * // In menu.config.yaml:
 * // shortcuts:
 * //   'Ctrl+Shift+N': 'insert.customElement'
 * //   'F1': 'help.show'
 * 
 * // Or programmatically (for dynamic bindings):
 * guiAdapter.bindShortcuts({
 *   'Ctrl+Alt+C': 'circuit.compile',
 *   'F2': 'element.properties'
 * });
 *
 * @example
 * // Listening to adapter state changes
 * guiAdapter.circuitService.on('update', (event) => {
 *   console.log('Circuit updated:', event.type);
 *   // Custom reaction to circuit changes
 * });
 *
 * // Accessing current interaction state
 * const isWireMode = guiAdapter.wireDrawingMode;
 * const placingElement = guiAdapter.placingElement;
 * const selectedElements = guiAdapter.circuitRenderer.getSelectedElements();
 */
export class GUIAdapter {
  /**
   * @constructor
   * @param {?HTMLElement} _controls Deprecated. Kept for signature compatibility; not used.
   * @param {!HTMLCanvasElement} canvas Canvas for circuit rendering.
   * @param {!CircuitService} circuitService Core domain service (aggregate orchestration, emits "update").
   * @param {!ElementRegistry} elementRegistry Registry of circuit components (DI).
   * @param {!RendererFactory} rendererFactory Factory for creating renderers (DI).
   * @param {!GUICommandRegistry} guiCommandRegistry Registry for GUI commands (DI).
   */
  constructor(
    _controls,
    canvas,
    circuitService,
    elementRegistry,
    rendererFactory,
    guiCommandRegistry,
  ) {
    /**
     * @private
     * @type {!HTMLCanvasElement}
     */
    this.canvas = canvas;
    /**
     * @private
     * @type {!CircuitService}
     */
    this.circuitService = circuitService;
    /**
     * @private
     * @type {!ElementRegistry}
     */
    this.elementRegistry = elementRegistry;

    /**
     * @private
     * @type {!CircuitRenderer}
     */
    this.circuitRenderer = new CircuitRenderer(
      canvas,
      circuitService,
      rendererFactory,
      () => this.activeCommand !== null, // Function to check if there's an active command
    );

    /**
     * @private
     * @type {!GUICommandRegistry}
     */
    this.guiCommandRegistry = guiCommandRegistry;

    /**
     * @private
     * @type {!CommandHistory}
     */
    this.commandHistory = new CommandHistory();

    // Interaction state
    /**
     * @private
     * @type {?Command}
     */
    this.activeCommand = null;
    /**
     * @private
     * @type {boolean}
     */
    this.hasDragged = false;
    /** @private */
    this.mouseDownPos = { x: 0, y: 0 };
    /** @private */
    this.wireDrawingMode = false; // Wire drawing mode state
    /** @private */
    this.placingElement = null;
    /** @private */
    this.selectionBox = null; // Selection box state: { startX, startY, endX, endY }
    /** @private */
    this.isSelecting = false; // True when drawing selection box
    /** @private */
    this.currentMousePos = { x: 0, y: 0 }; // Track current mouse position for immediate placement

    // Listener refs for clean disposal
    /** @private */ this._onMenuAction = null;
    /** @private */ this._onKeydown = null;
    /** @private */ this._onWheel = null;
    /** @private */ this._onImageLoaded = null;
  }

  /**
   * **🚀 Initialization Method** - Sets up the complete adapter with all bindings.
   *
   * Establishes the hexagonal architecture connections:
   * - Menu system → action handling
   * - Keyboard shortcuts → action routing
   * - Mouse/touch interactions → command execution
   * - Event system → automatic UI updates
   *
   * **Extension Points Initialized:**
   * - Menu action bindings (config-driven)
   * - Keyboard shortcut mapping
   * - Canvas interaction handlers
   * - Property panel integration
   * - Domain event listeners for UI updates
   *
   * Call this after creating the adapter to activate all functionality.
   *
   * @example
   * const adapter = new GUIAdapter(canvas, circuitService, elementRegistry,
   *                                rendererFactory, commandRegistry);
   * adapter.initialize(); // Activates all interactions
   */
  initialize() {
    // Declarative inputs
    this.bindMenu();
    this.bindShortcuts(KEYMAP);
    this.bindWheelZoom();
    this.bindImageLoadEvents();

    // Pointer interactions on canvas
    this.setupCanvasInteractions();

    // Property panel integration
    this.setupPropertyPanel();

    // Re-render on domain state changes
    this.circuitService.on("update", () => this.circuitRenderer.render());

    // Perform initial render
    this.circuitRenderer.render();
  }

  /**
   * Unhook global listeners (useful for hot-reload/tests).
   */
  dispose() {
    if (this._onMenuAction) document.removeEventListener("ui:action", this._onMenuAction);
    if (this._onKeydown) document.removeEventListener("keydown", this._onKeydown);
    if (this._onWheel) this.canvas.removeEventListener("wheel", this._onWheel);
    if (this._onImageLoaded) document.removeEventListener("renderer:imageLoaded", this._onImageLoaded);
  }

  /* ---------------------------------------------------------------------- */
  /*                               BINDERS                                  */
  /* ---------------------------------------------------------------------- */

  /**
   * Bind config-driven menubar events to action handling.
   * The MenuBar emits: CustomEvent('ui:action', { detail: { id } }).
   */
  bindMenu() {
    this._onMenuAction = (e) => this.handleAction(e.detail.id);
    document.addEventListener("ui:action", this._onMenuAction);
  }

  /**
   * Bind global keyboard shortcuts to the same action ids from config.
   * @param {!Object<string,string>} keymap Map of "Ctrl+X" → "action.id"
   */
  bindShortcuts(keymap) {
    const signature = (e) => {
      const ctrl = e.ctrlKey || e.metaKey ? "Ctrl+" : "";
      const shift = e.shiftKey ? "Shift+" : "";
      let key = e.key;
      
      // Map arrow keys to text names to match menu config
      const arrowMap = {
        'ArrowRight': 'Right',
        'ArrowLeft': 'Left', 
        'ArrowUp': 'Up',
        'ArrowDown': 'Down'
      };
      
      // Map special keys to match menu config names
      const specialKeyMap = {
        'Delete': 'Del',
        'Backspace': 'Del'  // Both Del and Backspace should trigger delete
      };
      
      if (arrowMap[key]) {
        key = arrowMap[key];
      } else if (specialKeyMap[key]) {
        key = specialKeyMap[key];
      } else if (key.length === 1) {
        key = key.toUpperCase();
      }
      
      return ctrl + shift + key;
    };

    this._onKeydown = (e) => {
      // Handle Escape key to cancel wire drawing mode
      if (e.key === 'Escape' && this.wireDrawingMode) {
        this.resetCursor();
        e.preventDefault();
        return;
      }

      // Handle Escape key to cancel element placement
      if (e.key === 'Escape' && this.placingElement) {
        
        // Delete only the placing element, not all selected elements
        this.circuitService.deleteElement(this.placingElement.id);
        
        // Clear placement mode
        this.placingElement = null;
        // Clear selection since placement was cancelled
        this.circuitRenderer.setSelectedElements([]);
        this.circuitRenderer.render();
        e.preventDefault();
        return;
      }

      // Handle rotation keys during element placement
      if (this.placingElement && e.ctrlKey) {
        let rotationAngle = 0;
        
        if (e.key === 'ArrowRight') {
          rotationAngle = 90; // Rotate 90° clockwise
          e.preventDefault();
        } else if (e.key === 'ArrowLeft') {
          rotationAngle = -90; // Rotate 90° counterclockwise
          e.preventDefault();
        } else if (e.key === 'ArrowUp') {
          rotationAngle = 180; // Rotate 180°
          e.preventDefault();
        } else if (e.key === 'ArrowDown') {
          rotationAngle = 180; // Rotate 180° (same as up for simple elements)
          e.preventDefault();
        }

        if (rotationAngle !== 0) {
          this.rotatePlacingElement(rotationAngle);
          return;
        }
      }

      const sig = signature(e);
      const id = keymap[sig];
      if (!id) return;
      e.preventDefault();
      this.handleAction(id);
    };

    document.addEventListener("keydown", this._onKeydown);
  }

  /**
   * Convert Ctrl+wheel into renderer zoom steps (declarative equivalent).
   * Leaves normal scrolling alone if Ctrl is not pressed.
   */
  bindWheelZoom() {
    // Let CircuitRenderer handle wheel events directly - it has its own zoom method
    // No need to bind wheel events here as CircuitRenderer.initEventListeners() handles them
  }

  /**
   * Bind renderer image load events to trigger re-renders when images finish loading.
   */
  bindImageLoadEvents() {
    this._onImageLoaded = (event) => {
      // Re-render when any renderer image loads
      this.circuitRenderer.render();
    };
    document.addEventListener("renderer:imageLoaded", this._onImageLoaded);
  }

  /* ---------------------------------------------------------------------- */
  /*                          ACTION ROUTING (DECLARATIVE)                  */
  /* ---------------------------------------------------------------------- */

  /**
   * **🎯 Primary Action Handler** - Main extension point for custom actions.
   *
   * Routes semantic action IDs from configuration to appropriate handlers.
   * This is where developers can intercept and customize user interactions.
   *
   * **Action Flow:**
   * 1. UI emits semantic action ID (from menu.config.yaml)
   * 2. GUIAdapter maps ID to action specification
   * 3. Executes appropriate command, renderer operation, or history action
   *
   * **Extension Pattern:**
   * Add new actions in menu.config.yaml, register commands in GUICommandRegistry.
   *
   * @param {string} id - Action identifier (e.g., "edit.undo", "insert.resistor")
   *
   * @example
   * // Programmatic action triggering
   * guiAdapter.handleAction('insert.resistor');
   * guiAdapter.handleAction('edit.undo');
   *
   * @example
   * // Custom action handling (extend via configuration)
   * // In menu.config.yaml:
   * // actions:
   * //   'custom.optimize': { kind: 'command', name: 'optimizeCircuit' }
   * guiAdapter.handleAction('custom.optimize');
   */
  handleAction(id) {
    /** @type {ActionSpec|undefined} */
    const spec = ACTIONS[id];
    if (!spec) {
      console.warn("[GUIAdapter] Unhandled action id:", id);
      return;
    }
    if (spec.kind === "disabled") {
      console.warn(`[GUIAdapter] '${id}' is disabled.`);
      return;
    }

    this._exec(spec);

    // Note: Domain operations (commands) will emit "update" → render() automatically via event listener.
    // Only render manually for UI-only operations that don't trigger domain events.
    if (spec.kind !== "command") {
      this.circuitRenderer.render();
    }
  }

  /**
   * Execute a declarative action spec (from YAML/JSON).
   * @param {!ActionSpec} spec
   * @private
   */
  _exec(spec) {
    switch (spec.kind) {
      case "command": {
        // Special handling for wire drawing mode
        if (spec.name === "addElement" && spec.args && spec.args[0] === "Wire") {
          this.wireDrawingMode = true;
          this.setCrosshairCursor();
          return;
        }

        // If user is creating a non-wire element while in wire drawing mode, exit wire mode
        if (spec.name === "addElement" && this.wireDrawingMode && spec.args && spec.args[0] !== "Wire") {
          this.resetCursor();
        }

        const args = spec.args ?? [];
        const cmd = this.guiCommandRegistry.get(
          spec.name,
          this.circuitService,
          this.circuitRenderer,
          this.elementRegistry,
          ...args
        );
        if (!cmd) {
          console.warn("[GUIAdapter] Missing command:", spec.name, args);
          return;
        }
        
        // Special handling for addElement: pass current mouse position for grid alignment
        if (spec.name === "addElement" && cmd.setMousePosition && this.currentMousePos) {
          cmd.setMousePosition(this.currentMousePos);
        }
        
        this.commandHistory.executeCommand(cmd, this.circuitService);
        break;
      }

      case "history": {
        if (spec.op === "undo") this.commandHistory.undo(this.circuitService);
        else if (spec.op === "redo") this.commandHistory.redo(this.circuitService);
        else console.warn("[GUIAdapter] Unknown history op:", spec.op);
        break;
      }

      case "renderer": {
        // Support special shorthands (no need to expose renderer internals in config)
        if (spec.op === "zoomStep") {
          this._zoomStep(spec.args?.[0] ?? +1);
          break;
        }
        if (spec.op === "recenter") {
          this._recenterView();
          break;
        }
        // Power-user path: call renderer method by name if it exists
        if (typeof this.circuitRenderer[spec.op] === "function") {
          this.circuitRenderer[spec.op](...(spec.args ?? []));
        } else {
          console.warn("[GUIAdapter] Unknown renderer op:", spec.op);
        }
        break;
      }

      case "todo": {
        console.warn("[GUIAdapter] TODO:", spec.note ?? "(no note)");
        break;
      }

      case "disabled": {
        // No-op for disabled menu items
        break;
      }

      default: {
        console.warn("[GUIAdapter] Unknown action kind:", /** @type {*} */(spec).kind, spec);
      }
    }
  }

  /**
   * Renderer zoom helper; mimics your existing `zoom(event)` API.
   * @param {number} sign +1 for out, -1 for in
   * @private
   */
  _zoomStep(sign) {
    // Get canvas center for zoom focus when using menu commands
    const rect = this.canvas.getBoundingClientRect();
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    
    const evtLike = {
      deltaY: sign > 0 ? 100 : -100,
      ctrlKey: true,
      offsetX: centerX,
      offsetY: centerY,
      preventDefault() {}
    };
    this.circuitRenderer.zoom(evtLike);
  }

  /**
   * Recenter the view; prefers a renderer method if available, otherwise resets transform.
   * @private
   */
  _recenterView() {
    if (typeof this.circuitRenderer.reCenter === "function") {
      this.circuitRenderer.reCenter();
      return;
    }
    // Fallback: reset to defaults expected by your interaction math.
    this.circuitRenderer.scale = 1;
    this.circuitRenderer.offsetX = 0;
    this.circuitRenderer.offsetY = 0;
  }

  /* ---------------------------------------------------------------------- */
  /*                        CANVAS INTERACTIONS (UNCHANGED)                  */
  /* ---------------------------------------------------------------------- */

  /**
   * Sets up mouse events for interaction on the canvas: pan (MMB), drag, draw, placement.
   * Note: Ctrl+wheel zoom is handled by bindWheelZoom(); regular wheel is left to the page.
   */
  setupCanvasInteractions() {
    // Middle-mouse pan start
    this.canvas.addEventListener("mousedown", (event) => {
      if (event.button === 1) {
        this.canvas.style.cursor = "grabbing";
        this.panStartX = event.clientX - this.circuitRenderer.offsetX;
        this.panStartY = event.clientY - this.circuitRenderer.offsetY;
        return;
      }

      const { offsetX, offsetY } = this.getTransformedMousePosition(event);

      // If placing an element, finalize its position on left click
      if (event.button === 0 && this.placingElement) {
        const snappedX = GRID_CONFIG.snapToGrid(offsetX);
        const snappedY = GRID_CONFIG.snapToGrid(offsetY);

        // Get current orientation from element properties (preserve rotation)
        const currentOrientation = this.placingElement.properties?.values?.orientation || 0;
        const angleRad = (currentOrientation * Math.PI) / 180;
        
        // Use grid configuration to calculate proper node positions that align to grid
        const nodePositions = GRID_CONFIG.calculateNodePositions(snappedX, snappedY, angleRad);
        this.placingElement.nodes[0].x = nodePositions.start.x;
        this.placingElement.nodes[0].y = nodePositions.start.y;
        this.placingElement.nodes[1].x = nodePositions.end.x;
        this.placingElement.nodes[1].y = nodePositions.end.y;

        this.circuitService.emit("update", {
          type: "finalizePlacement",
          element: this.placingElement,
        });

        // Store reference to placed element for property panel
        const placedElement = this.placingElement;
        
        this.placingElement = null;
        // Keep the placed element selected for user convenience
        this.circuitRenderer.setSelectedElements([placedElement]);
        this.circuitRenderer.render();
        
        // Open property panel immediately after placing element
        this.handleElementDoubleClick(placedElement, true); // true indicates this is a newly placed element
        
        return;
      }

      // Regular command start
      if (event.button === 0) {
        // Wire drawing mode takes priority over all other interactions
        if (this.wireDrawingMode) {
          // In wire drawing mode, always start wire drawing regardless of what's under the cursor
          // This allows drawing wire nodes on top of existing nodes/elements
          this.activeCommand = this.guiCommandRegistry.get(
            "drawWire",
            this.circuitService,
            this.elementRegistry,
          );
        } else {
          // Normal interaction logic when not in wire drawing mode
          const element = this.findElementAt(offsetX, offsetY);

          // If clicking on an element, select it first
          if (element) {
            const selectCommand = this.guiCommandRegistry.get("selectElement");
            if (selectCommand) {
              selectCommand.execute(element);
            }
          }

          // Determine which command to start based on context
          if (element) {
            // Clicking on an element starts drag
            this.activeCommand = this.guiCommandRegistry.get("dragElement", this.circuitService);
          } else {
            // Clicking on empty space starts selection box
            this.startSelectionBox(offsetX, offsetY);
            this.activeCommand = null;
          }
        }

        if (this.activeCommand) {
          const before = this.circuitService.exportState();
          this.activeCommand.start(offsetX, offsetY);
          this.activeCommand.beforeSnapshot = before;
        }

        this.hasDragged = false;
        this.mouseDownPos = { x: offsetX, y: offsetY };
      }
    });

    // Move / live placement preview / command move
    this.canvas.addEventListener("mousemove", (event) => {
      const { offsetX, offsetY } = this.getTransformedMousePosition(event);
      
      // Always track current mouse position for immediate element placement
      this.currentMousePos.x = offsetX;
      this.currentMousePos.y = offsetY;

      // Live update for placing element
      if (this.placingElement) {
        const snappedX = GRID_CONFIG.snapToGrid(offsetX);
        const snappedY = GRID_CONFIG.snapToGrid(offsetY);

        // Get current orientation from element properties (preserve rotation)
        const currentOrientation = this.placingElement.properties?.values?.orientation || 0;
        const angleRad = (currentOrientation * Math.PI) / 180;
        
        // Use grid configuration to calculate proper node positions that align to grid
        const nodePositions = GRID_CONFIG.calculateNodePositions(snappedX, snappedY, angleRad);
        this.placingElement.nodes[0].x = nodePositions.start.x;
        this.placingElement.nodes[0].y = nodePositions.start.y;
        this.placingElement.nodes[1].x = nodePositions.end.x;
        this.placingElement.nodes[1].y = nodePositions.end.y;

        this.circuitService.emit("update", {
          type: "movePreview",
          element: this.placingElement,
        });

        return;
      }

      // Update selection box if selecting
      if (this.isSelecting) {
        this.updateSelectionBox(offsetX, offsetY);
        return;
      }

      // Regular move for active command
      if (this.activeCommand) {
        const dx = offsetX - this.mouseDownPos.x;
        const dy = offsetY - this.mouseDownPos.y;
        if (Math.sqrt(dx * dx + dy * dy) > 2) {
          this.hasDragged = true;
        }
        this.activeCommand.move(offsetX, offsetY);
      }
    });

    // Mouse up → finalize command / snapshot for undo
    this.canvas.addEventListener("mouseup", (event) => {
      if (event.button === 1) {
        this.canvas.style.cursor = "default";
        return;
      }

      const { offsetX, offsetY } = this.getTransformedMousePosition(event);

      // Finalize selection box if selecting
      if (this.isSelecting) {
        this.finalizeSelection();
        return;
      }

      if (this.activeCommand) {
        const before = this.activeCommand.beforeSnapshot;
        const wasWireDrawing = this.activeCommand.constructor.name === 'DrawWireCommand';

        if (!this.hasDragged && this.activeCommand.cancel) {
          this.activeCommand.cancel();
        } else {
          this.activeCommand.stop();
          const after = this.circuitService.exportState();

          if (this.hasStateChanged(before, after)) {
            // Convert the delta into a single undoable snapshot command
            this.circuitService.importState(before);

            const snapshotCommand = {
              execute: () => this.circuitService.importState(after),
              undo: () => this.circuitService.importState(before),
            };

            this.commandHistory.executeCommand(snapshotCommand, this.circuitService);
          }
        }

        // Reset wire drawing mode after completing a wire
        if (wasWireDrawing && this.wireDrawingMode) {
          this.resetCursor();
        }

        this.activeCommand = null;
      }
    });

    // Listen to the element placement event
    this.circuitService.on("startPlacing", ({ element }) => {
      this.placingElement = element;
      
      // Clear existing selections and select only the placing element
      // This ensures rotation during placement only affects the placing element
      this.circuitRenderer.setSelectedElements([element]);
      
      // Immediately position the element at the current mouse position
      // This prevents the element from staying at default coordinates until mouse movement
      const snappedX = GRID_CONFIG.snapToGrid(this.currentMousePos.x);
      const snappedY = GRID_CONFIG.snapToGrid(this.currentMousePos.y);

      // Get current orientation from element properties (preserve rotation)
      const currentOrientation = element.properties?.values?.orientation || 0;
      const angleRad = (currentOrientation * Math.PI) / 180;
      
      // Use grid configuration to calculate proper node positions that align to grid
      const nodePositions = GRID_CONFIG.calculateNodePositions(snappedX, snappedY, angleRad);
      element.nodes[0].x = nodePositions.start.x;
      element.nodes[0].y = nodePositions.start.y;
      element.nodes[1].x = nodePositions.end.x;
      element.nodes[1].y = nodePositions.end.y;

      // Emit update to immediately show the element at the correct position
      this.circuitService.emit("update", {
        type: "movePreview",
        element: element,
      });
      
      // If user starts placing a non-wire element while in wire drawing mode, exit wire mode
      if (this.wireDrawingMode && element.type !== 'wire') {
        this.resetCursor();
      }
    });
  }

  /**
   * Sets up property panel integration with double-click handling.
   */
  setupPropertyPanel() {
    // Set up double-click callback on the circuit renderer
    this.circuitRenderer.setElementDoubleClickCallback((element) => {
      this.handleElementDoubleClick(element);
    });
  }

  /**
   * Handles double-click on circuit elements to open property panel.
   * @param {Object} element - The clicked circuit element
   * @param {boolean} isNewlyPlaced - Whether this element was just placed (true) or is being edited (false)
   */
  handleElementDoubleClick(element, isNewlyPlaced = false) {
    if (!element) return;
    
    // Skip property panel for ground elements - they don't need labeling or property editing
    if (element.type === 'ground') {
      return;
    }

    // Open property panel for the element
    this.propertyPanel = new PropertyPanel();
    this.propertyPanel.show(element,
      // onSave callback
      (element, updatedProperties) => {
        // Handle property save - get fresh command instance
        const command = this.guiCommandRegistry.get('updateElementProperties');
        if (command) {
          command.setData(element.id, updatedProperties);
          this.commandHistory.executeCommand(command, this.circuitService);
        }
      },
      // onCancel callback
      () => {
        if (isNewlyPlaced) {
          // If this element was just placed and user cancelled, delete it
          // Delete only the specific element, not all selected elements
          this.circuitService.deleteElement(element.id);
          // Clear selections since we deleted the element
          this.circuitRenderer.setSelectedElements([]);
          this.circuitRenderer.render();
        }
      }
    );
  }

  /**
   * **📐 Coordinate Transformation** - Converts screen to world coordinates.
   *
   * Essential for mouse interactions on the canvas. Accounts for:
   * - Canvas position in viewport
   * - Renderer pan offset
   * - Renderer zoom scale
   *
   * Use this for all mouse position calculations in extensions.
   *
   * @param {!MouseEvent} event - Mouse event from canvas
   * @returns {{offsetX:number, offsetY:number}} World coordinates
   *
   * @example
   * canvas.addEventListener('click', (event) => {
   *   const { offsetX, offsetY } = adapter.getTransformedMousePosition(event);
   *   // Use world coordinates for element placement, hit testing, etc.
   * });
   */
  getTransformedMousePosition(event) {
    const rect = this.canvas.getBoundingClientRect();
    return {
      offsetX:
        (event.clientX - rect.left - this.circuitRenderer.offsetX) /
        this.circuitRenderer.scale,
      offsetY:
        (event.clientY - rect.top - this.circuitRenderer.offsetY) /
        this.circuitRenderer.scale,
    };
  }

  /**
   * **🎯 Element Selection Logic** - Smart element detection at coordinates.
   *
   * Implements intelligent selection priorities:
   * 1. **Node Proximity**: Elements with nodes near click point rank higher
   * 2. **Type Priority**: Non-wire elements preferred over wires
   * 3. **Hit Testing**: Uses element-specific intersection algorithms
   *
   * This method is crucial for user experience - determines which element
   * responds to clicks when multiple elements overlap.
   *
   * **Extension Point**: Override for custom selection behavior.
   *
   * @param {number} worldX - World X coordinate
   * @param {number} worldY - World Y coordinate
   * @returns {?Object} Best matching element or null
   *
   * @example
   * // Custom selection logic
   * const element = adapter.findElementAt(mouseX, mouseY);
   * if (element?.type === 'myCustomType') {
   *   // Handle custom element interaction
   * }
   */
  findElementAt(worldX, worldY) {
    const elements = this.circuitService.getElements();
    const nodeProximityThreshold = 15; // Distance to prioritize node-based selection

    let bestElement = null;
    let bestScore = -1;

    for (const element of elements) {
      if (!this.isInsideElement(worldX, worldY, element)) continue;

      // Calculate selection score based on proximity to nodes and element type
      let score = 0;

      // Check if click is close to any node of this element
      let closestNodeDistance = Infinity;
      if (Array.isArray(element.nodes)) {
        for (const node of element.nodes) {
          const distance = Math.hypot(worldX - node.x, worldY - node.y);
          closestNodeDistance = Math.min(closestNodeDistance, distance);
        }
      }

      // Higher score for elements with nodes close to click point
      if (closestNodeDistance <= nodeProximityThreshold) {
        score += 100 - closestNodeDistance; // Closer nodes get higher scores
      }

      // Prioritize non-wire elements over wires
      if (element.type !== 'wire') {
        score += 50;
      }

      // Update best element if this one has a higher score
      if (score > bestScore) {
        bestScore = score;
        bestElement = element;
      }
    }

    return bestElement;
  }

  /**
   * Hit-test for a line-like element with an "aura".
   * @param {number} x
   * @param {number} y
   * @param {!Object} element
   * @returns {boolean}
   */
  isInsideElement(x, y, element) {
    if (element.nodes.length < 2) return false;
    const aura = 10;
    const [start, end] = element.nodes;
    const dx = end.x - start.x;
    const dy = end.y - start.y;
    const length = Math.hypot(dx, dy);
    if (length < 1e-6) return Math.hypot(x - start.x, y - start.y) <= aura;
    const distance =
      Math.abs(dy * x - dx * y + end.x * start.y - end.y * start.x) / length;
    if (distance > aura) return false;
    const minX = Math.min(start.x, end.x) - aura;
    const maxX = Math.max(start.x, end.x) + aura;
    const minY = Math.min(start.y, end.y) - aura;
    const maxY = Math.max(start.y, end.y) + aura;
    return !(x < minX || x > maxX || y < minY || y > maxY);
  }

  /**
   * Shallow state delta check for snapshotting (undo batching).
   * @param {string|Object} before JSON string or object
   * @param {string|Object} after JSON string or object
   * @returns {boolean}
   */
  hasStateChanged(before, after) {
    before = typeof before === "string" ? JSON.parse(before) : before;
    after  = typeof after  === "string" ? JSON.parse(after)  : after;
    if (!before || !after) return true;
    if (before.elements.length !== after.elements.length) return true;
    for (let i = 0; i < before.elements.length; i++) {
      const a = before.elements[i];
      const b = after.elements[i];
      if (a.id !== b.id || a.type !== b.type) return true;
      if (JSON.stringify(a.nodes) !== JSON.stringify(b.nodes)) return true;
    }
    return false;
  }

  /**
   * Start selection box drawing
   * @param {number} x World X coordinate
   * @param {number} y World Y coordinate
   * @private
   */
  startSelectionBox(x, y) {
    this.isSelecting = true;
    this.selectionBox = {
      startX: x,
      startY: y,
      endX: x,
      endY: y
    };
  }

  /**
   * Update selection box during drag
   * @param {number} x World X coordinate
   * @param {number} y World Y coordinate
   * @private
   */
  updateSelectionBox(x, y) {
    if (!this.selectionBox) return;
    
    this.selectionBox.endX = x;
    this.selectionBox.endY = y;
    
    // Pass selection box to renderer
    this.circuitRenderer.setSelectionBox(this.selectionBox);
    
    // Trigger re-render to show selection box
    this.circuitRenderer.render();
  }

  /**
   * Finalize selection and select elements within box
   * @private
   */
  finalizeSelection() {
    if (!this.selectionBox) return;

    const { startX, startY, endX, endY } = this.selectionBox;
    
    // Normalize box coordinates
    const left = Math.min(startX, endX);
    const right = Math.max(startX, endX);
    const top = Math.min(startY, endY);
    const bottom = Math.max(startY, endY);

    // Check if this was just a click (very small selection box)
    const boxWidth = right - left;
    const boxHeight = bottom - top;
    const isJustClick = boxWidth <= 5 && boxHeight <= 5;

    // Find elements within selection box
    const selectedElements = this.circuitService.getElements().filter(element => {
      return this.isElementInBox(element, left, top, right, bottom);
    });

    if (selectedElements.length > 0) {
      // Select all elements using multi-select command
      const multiSelectCommand = this.guiCommandRegistry.get("multiSelectElement");
      if (multiSelectCommand) {
        multiSelectCommand.execute(selectedElements);
      }
    } else if (isJustClick) {
      // If it was just a click on empty space, deselect all
      const deselectAllCommand = this.guiCommandRegistry.get("deselectAll");
      if (deselectAllCommand) {
        deselectAllCommand.execute();
      }
    }


    // Clean up selection state
    this.isSelecting = false;
    this.selectionBox = null;
    
    // Clear selection box from renderer
    this.circuitRenderer.setSelectionBox(null);
    
    // Re-render to clear selection box and show selected elements
    this.circuitRenderer.render();
  }

  /**
   * Check if an element is within the selection box
   * @param {Object} element Circuit element
   * @param {number} left Left boundary
   * @param {number} top Top boundary  
   * @param {number} right Right boundary
   * @param {number} bottom Bottom boundary
   * @returns {boolean}
   * @private
   */
  isElementInBox(element, left, top, right, bottom) {
    if (!element.nodes || element.nodes.length === 0) return false;

    // Check if any node of the element is within the box
    return element.nodes.some(node => {
      return node.x >= left && node.x <= right && 
             node.y >= top && node.y <= bottom;
    });
  }

  /**
   * Set crosshair cursor for wire drawing mode
   * @private
   */
  setCrosshairCursor() {
    if (this.canvas && this.canvas.style) {
      this.canvas.style.cursor = 'crosshair';
    }
  }

  /**
   * Reset cursor to default and deactivate wire drawing mode
   * @private
   */
  resetCursor() {
    if (this.canvas && this.canvas.style) {
      this.canvas.style.cursor = 'default';
    }
    this.wireDrawingMode = false;
  }

  /**
   * Rotate the element currently being placed
   * @param {number} angle - Rotation angle in degrees (90, -90, 180, etc.)
   * @private
   */
  rotatePlacingElement(angle) {
    if (!this.placingElement) return;

    
    // Initialize properties if they don't exist
    if (!this.placingElement.properties) {
      console.warn("[GUIAdapter] Element missing properties, cannot set orientation");
    } else {
      // Initialize properties.values if it doesn't exist
      if (!this.placingElement.properties.values) {
        this.placingElement.properties.values = {};
      }
      
      // Update element's orientation property
      const currentOrientation = this.placingElement.properties.values.orientation || 0;
      this.placingElement.properties.values.orientation = (currentOrientation + angle) % 360;
      
      // Normalize negative angles
      if (this.placingElement.properties.values.orientation < 0) {
        this.placingElement.properties.values.orientation += 360;
      }
    }
    
    // Get current element center
    const centerX = (this.placingElement.nodes[0].x + this.placingElement.nodes[1].x) / 2;
    const centerY = (this.placingElement.nodes[0].y + this.placingElement.nodes[1].y) / 2;
    
    // For most components, rotation changes the node positions
    const angleRad = (angle * Math.PI) / 180;
    const currentAngleRad = Math.atan2(
      this.placingElement.nodes[1].y - this.placingElement.nodes[0].y,
      this.placingElement.nodes[1].x - this.placingElement.nodes[0].x
    );
    const newAngleRad = currentAngleRad + angleRad;
    
    // Use grid configuration to calculate proper node positions that align to grid
    const nodePositions = GRID_CONFIG.calculateNodePositions(centerX, centerY, newAngleRad);
    this.placingElement.nodes[0].x = nodePositions.start.x;
    this.placingElement.nodes[0].y = nodePositions.start.y;
    this.placingElement.nodes[1].x = nodePositions.end.x;
    this.placingElement.nodes[1].y = nodePositions.end.y;
    
    // Emit update event for rotation
    this.circuitService.emit('update', {
      type: 'rotatePlacingElement',
      element: this.placingElement,
    });
    
    // Force immediate re-render to show rotation
    this.circuitRenderer.render();
  }
}