domain/aggregates/Circuit.js

/**
 * @class Circuit
 * @description
 * Domain aggregate representing a complete circuit design in the JSCircuit Editor.
 *
 * This is the core aggregate in our Domain-Driven Design architecture that maintains
 * the integrity of a circuit by managing elements and their connections. It enforces
 * business rules such as element uniqueness and connection validity.
 *
 * **Key Responsibilities:**
 * - Manage circuit elements (resistors, capacitors, wires, etc.)
 * - Validate element additions and connections
 * - Maintain connection mapping between elements
 * - Enforce domain business rules and constraints
 *
 * **Aggregate Root Pattern:**
 * As an aggregate root, Circuit is the only way to access and modify circuit elements.
 * All mutations must go through this aggregate to maintain consistency.
 *
 * @example
 * const circuit = new Circuit();
 * const resistor = new Resistor('R1', [pos1, pos2], null, new Properties({resistance: 1000}));
 * circuit.validateAddElement(resistor);
 * circuit.addElement(resistor);
 */
export class Circuit {
    /**
     * Creates a new empty circuit.
     *
     * Initializes the circuit with empty collections for elements and connections.
     * The circuit starts in a valid state with no elements or connections.
     */
    constructor() {
        this.elements = []; // List of all elements in the circuit
        this.connections = new Map(); // Map of node positions to connected elements
    }

    /**
     * Validates whether an element can be added to the circuit.
     *
     * @param {Element} element - The element to validate.
     * @throws {Error} If the element violates circuit rules.
     */
    validateAddElement(element) {
        // Check that the element is unique
        if (this.elements.some(el => el.id === element.id)) {
            throw new Error(`Element with id ${element.id} is already in the circuit.`);
        }
    }

    /**
     * Validates and establishes a connection between two elements in the circuit.
     * This method handles two types of connections:
     * - Node-to-Node connections: Direct connections between nodes of two elements.
     * - Node-to-Wire-Body connections: A node connecting to any point along a wire's body.
     *
     * @param {Element} element1 - The first element to connect.
     * @param {Element} element2 - The second element to connect.
     * @throws {Error} If the connection violates circuit rules.
     */
    validateConnection(element1, element2) {
        // Handle node-to-node connections
        const element1NodeKeys = new Set(element1.nodes.map(node => `${node.x},${node.y}`));
    
        for (const node of element2.nodes) {
            const key = `${node.x},${node.y}`;
    
            if (element1NodeKeys.has(key)) {
                const connectedElements = this.connections.get(key) || [];
    
                // Check if the node is already connected to other elements
                const isAlreadyConnected = connectedElements.some(
                    connectedElement => connectedElement !== element1 && connectedElement !== element2
                );
    
                if (isAlreadyConnected) {
                    throw new Error(
                        `Node at position (${node.x}, ${node.y}) is already connected and cannot accept additional connections.`
                    );
                }
    
                // Add the connection
                if (!this.connections.has(key)) {
                    this.connections.set(key, []);
                }
                this.connections.get(key).push(element1, element2);
            }
        }
    
        // Handle node-to-wire-body connections
        const wire = element1.type === 'wire' ? element1 : element2.type === 'wire' ? element2 : null;
        const node = element1.type === 'wire' ? element2.nodes[0] : element1.nodes[0];
    
        if (wire && node) {
            // Check if the node lies on any segment of the wire
            for (let i = 0; i < wire.nodes.length - 1; i++) {
                const start = wire.nodes[i];
                const end = wire.nodes[i + 1];
    
                if (this.isNodeOnWireSegment(node, start, end)) {
                    const key = `${node.x},${node.y}`;
                    const connectedElements = this.connections.get(key) || [];
    
                    // Ensure no duplicate connections
                    if (connectedElements.some(e => e === node)) {
                        throw new Error(`Node at position (${node.x}, ${node.y}) is already connected and cannot accept additional connections.`);
                    }
    
                    if (!this.connections.has(key)) {
                        this.connections.set(key, []);
                    }
                    this.connections.get(key).push(wire, node);
                    return; // Exit after finding a valid connection
                }
            }
        }
    }

    /**
     * Checks if a node lies on the body of a wire, defined by a line segment between two endpoints.
     *
     * This method is used to validate node-to-wire-body connections in a circuit.
     * It ensures that a node is either at one of the wire's terminal positions
     * or lies along any of the wire's intermediate line segments.
     *
     * @param {Position} node - The node to check.
     * @param {Position} wireStart - The starting point of the wire's line segment.
     * @param {Position} wireEnd - The ending point of the wire's line segment.
     * @returns {boolean} True if the node lies on the wire's line segment, otherwise false.
     */
    isNodeOnWireSegment(node, wireStart, wireEnd) {
        // Check if the node lies within the bounding box of the segment
        if (
            node.x < Math.min(wireStart.x, wireEnd.x) || node.x > Math.max(wireStart.x, wireEnd.x) ||
            node.y < Math.min(wireStart.y, wireEnd.y) || node.y > Math.max(wireStart.y, wireEnd.y)
        ) {
            return false;
        }

        // Use the collinearity condition: (y2 - y1) * (x - x1) == (y - y1) * (x2 - x1)
        const crossProduct = (wireEnd.y - wireStart.y) * (node.x - wireStart.x) - (node.y - wireStart.y) * (wireEnd.x - wireStart.x);

        // Check if the cross product is close to zero (to account for floating-point precision)
        if (Math.abs(crossProduct) > 1e-10) {
            return false;
        }

        return true;
    }

    /**
     * Serializes the circuit elements into a format suitable for export or persistence.
     *
     * This method converts all circuit elements into a plain JavaScript object format
     * that can be easily serialized to JSON or other data formats. It's primarily used
     * by export adapters like QucatNetlistAdapter for file operations.
     *
     * @returns {Object[]} Array of serialized element objects, each containing:
     *   - {string} id - The unique element identifier
     *   - {string} type - The element type (resistor, capacitor, wire, etc.)
     *   - {Object[]} nodes - Array of node positions as {x, y} objects
     *   - {Object} properties - Element properties as key-value pairs
     *   - {string|null} label - Element label text or null if no label
     *
     * @example
     * const serialized = circuit.getSerializedElements();
     * console.log(serialized[0]); // { id: 'R1', type: 'resistor', nodes: [{x: 10, y: 20}], ... }
     */
    getSerializedElements() {
        return this.elements.map(el => ({
            id: el.id,
            type: el.type,
            nodes: el.nodes.map(p => ({ x: p.x, y: p.y })),
            properties: el.properties.values,
            label: el.label ? el.label.value : null
        }));
    }
}