    
// swLine.js
// SWLine: A styled line segment class for SketchWaveJS
// Author: TechNoviceTools (TNT)
// Date: 2026-01-26
//
// This class represents a line segment between two SWPoints, with thickness and color.
// It is designed to be compatible with p5.js, SWColor, SWPoint, and SWGrid.
//
// Dependencies: SWColor, SWPoint, SWGrid, p5.js

console.log("[swLine.js] SWLine class loaded.");

class SWLine {
        
    /**
     * @param {SWPoint} ptA - First endpoint (SWPoint)
     * @param {SWPoint} ptB - Second endpoint (SWPoint)
     * @param {number} [thickness=2] - Stroke weight
     * @param {SWColor} [strokeColor] - Stroke color (SWColor instance)
     */
    constructor(ptA, ptB, thickness = 2, strokeColor = undefined) {
        this.ptA = ptA;
        this.ptB = ptB;
        this.thickness = thickness;
        this.strokeColor = strokeColor;
        this.shouldShowEndPoints = true;
        this.length = Math.sqrt(Math.pow(ptB.x - ptA.x, 2) + Math.pow(ptB.y - ptA.y, 2));
        if (ptB.x - ptA.x === 0) {
            this.slope = Infinity; //vertical line
            this.isVertical = true;
            this.isHorizontal = false;
        } else {
            this.slope = (ptB.y - ptA.y) / (ptB.x - ptA.x);
            this.isHorizontal = true;
            this.isVertical = false;
        }

        this.shouldShowSegment = true;

        // Store original endpoints for consistent rotation
        this.originalA = new SWPoint(ptA.x, ptA.y, ptA.z, ptA.strokeWeight, ptA.strokeColor);
        this.originalB = new SWPoint(ptB.x, ptB.y, ptB.z, ptB.strokeWeight, ptB.strokeColor);

        // Calculate midpoint
        const MIDPT_FACTOR = 2; //will be 2x thickness of line
        const midX = (ptA.x + ptB.x) / 2;
        const midY = (ptA.y + ptB.y) / 2;
        // Create a copy of strokeColor and darken it by 20%
        let midColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        if (midColor) midColor.darkenBy(0.2);
        this.midpoint = new SWPoint(midX, midY, undefined, MIDPT_FACTOR * thickness, midColor, "M");
        this.shouldShowMidpoint = true;
        this.midpoint.showLabel = true;
        if(this.midpoint.showLabel) {
            this.midpoint.setLabelOffsetFromSlope(this.slope, 15);
        }
    }//end constructor

    /**
     * Modulates the length of the line using a SWSinusoid instance, keeping the midpoint fixed.
     * @param {SWSinusoid} sinusoid - The sinusoid to apply
     * @param {number} t - The time or parameter to pass to the sinusoid
     */
    breathe(sinusoid, t) {
        // Find the current midpoint
        const midX = (this.ptA.x + this.ptB.x) / 2;
        const midY = (this.ptA.y + this.ptB.y) / 2;
        // Direction vector from ptA to ptB
        const dx = this.ptB.x - this.ptA.x;
        const dy = this.ptB.y - this.ptA.y;
        const len = Math.sqrt(dx * dx + dy * dy);
        if (len === 0) return; // Avoid division by zero
        // Unit direction vector
        const ux = dx / len;
        const uy = dy / len;
        // New half-length from sinusoid
        const newHalfLen = sinusoid.getValue(t) / 2;
        // Set new endpoints, keeping midpoint fixed
        this.ptA.x = midX - ux * newHalfLen;
        this.ptA.y = midY - uy * newHalfLen;
        this.ptB.x = midX + ux * newHalfLen;
        this.ptB.y = midY + uy * newHalfLen;
        // Update length and midpoint
        this.length = Math.sqrt(Math.pow(this.ptB.x - this.ptA.x, 2) + Math.pow(this.ptB.y - this.ptA.y, 2));
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
    }//end breathe

    /**
     * Modulates the length of the line using a SWSinusoid instance, keeping one endpoint fixed.
     * Creates a "bungee" effect where one end is anchored and the other oscillates toward/away from it.
     * @param {SWPoint} fixedPt - The endpoint to keep fixed (must be ptA or ptB)
     * @param {SWSinusoid} sinusoid - The sinusoid to apply (determines the length)
     * @param {number} t - The time or parameter to pass to the sinusoid
     * 
     * Usage: 
     *   line.breatheAbout(line.ptA, sinusoid, t); // ptA stays fixed, ptB oscillates
     *   line.breatheAbout(ptC, sinusoid, t); // When ptA = ptC, keeps center fixed
     */
    breatheAbout(fixedPt, sinusoid, t) {
        // Determine which endpoint is fixed and which is moving
        const isA = (fixedPt === this.ptA);
        const origFixed = isA ? this.originalA : this.originalB;
        const origMoving = isA ? this.originalB : this.originalA;
        
        // Direction vector from fixed to moving (using original positions)
        const dx = origMoving.x - origFixed.x;
        const dy = origMoving.y - origFixed.y;
        const origLen = Math.sqrt(dx * dx + dy * dy);
        
        if (origLen === 0) return; // Avoid division by zero
        
        // Unit direction vector
        const ux = dx / origLen;
        const uy = dy / origLen;
        
        // New length from sinusoid
        const newLen = sinusoid.getValue(t);
        
        // Calculate new position for moving endpoint
        const newX = origFixed.x + ux * newLen;
        const newY = origFixed.y + uy * newLen;
        
        // Set endpoints (fixed stays at original, moving gets new position)
        if (isA) {
            this.ptA.x = origFixed.x;
            this.ptA.y = origFixed.y;
            this.ptB.x = newX;
            this.ptB.y = newY;
        } else {
            this.ptB.x = origFixed.x;
            this.ptB.y = origFixed.y;
            this.ptA.x = newX;
            this.ptA.y = newY;
        }
        
        // Update midpoint and length
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end breatheAbout

    /**
     * Draws the line using p5.js in screen coordinates
     */
    draw() {
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        }
        strokeWeight(this.thickness);
        if (this.shouldShowSegment){
            line(this.ptA.x, this.ptA.y, this.ptB.x, this.ptB.y);
        }
        noStroke();
        strokeWeight(1);
        // Optionally, draw the midpoint
        if (this.midpoint && this.shouldShowMidpoint) {
            this.midpoint.draw();
        }
        // Optionally, draw the endpoints
        if (this.shouldShowEndPoints) {
            this.ptA.draw();
            this.ptB.draw();
        }
    }//end draw

    /**
     * Draws the line using user (grid) coordinates mapped by the given SWGrid
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const {x: x1, y: y1} = grid.userToScreen(this.ptA.x, this.ptA.y);
        const {x: x2, y: y2} = grid.userToScreen(this.ptB.x, this.ptB.y);
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        }
        strokeWeight(this.thickness);
        if (this.shouldShowSegment){
            line(x1, y1, x2, y2);
        }
        noStroke();
        strokeWeight(1);
        // Optionally, draw the midpoint
        if (this.midpoint && this.shouldShowMidpoint) {
            this.midpoint.drawOnGrid(grid);
        }
        // Optionally, draw the endpoints
        if (this.shouldShowEndPoints) {
            this.ptA.drawOnGrid(grid);
            this.ptB.drawOnGrid(grid);
        }
    }//end drawOnGrid

    /**
     * Rotates the line about its midpoint at a given angular velocity (deg/sec).
     * Positive degPerSec is counterclockwise, negative is clockwise.
     * @param {number} degPerSec - Angular velocity in degrees per second
     * @param {number} t - Time in seconds (or frameCount/framerate)
     *
     * Reasoning:
     * - The midpoint remains fixed.
     * - The endpoints are rotated around the midpoint by angle = degPerSec * t (converted to radians).
     * - This is useful for animating a spinning line, e.g., for oscilloscopes, clock hands, or dynamic geometry.
     *
     * Application:
     *   Call line.rotateAboutMidPoint(45, t) in your animation loop to rotate at 45 deg/sec.
     */
    rotateAboutMidPoint(degPerSec, t) {
        // Calculate the angle to rotate (in radians)
        const angleDeg = degPerSec * t;
        const angleRad = angleDeg * Math.PI / 180;
        // Always rotate from the original endpoints
        const midX = (this.originalA.x + this.originalB.x) / 2;
        const midY = (this.originalA.y + this.originalB.y) / 2;
        // Vector from midpoint to originalA and originalB
        const dxA = this.originalA.x - midX;
        const dyA = this.originalA.y - midY;
        const dxB = this.originalB.x - midX;
        const dyB = this.originalB.y - midY;
        // Rotate both endpoints around the midpoint
        this.ptA.x = midX + (dxA * Math.cos(angleRad) - dyA * Math.sin(angleRad));
        this.ptA.y = midY + (dxA * Math.sin(angleRad) + dyA * Math.cos(angleRad));
        this.ptB.x = midX + (dxB * Math.cos(angleRad) - dyB * Math.sin(angleRad));
        this.ptB.y = midY + (dxB * Math.sin(angleRad) + dyB * Math.cos(angleRad));
        // Update midpoint (should remain the same, but recalc for safety)
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
    }//end rotateAboutMidPoint

    /**
     * Returns a string representation
     */
    toString() {
        return `SWLine(ptA: ${this.ptA.toString()}, ptB: ${this.ptB.toString()}, thickness: ${this.thickness}, strokeColor: ${this.strokeColor ? this.strokeColor.toString() : 'none'}, length: ${this.length.toFixed(2)}, midpoint: ${this.midpoint ? this.midpoint.toString() : 'none'})`;
    }//end toString

    /**
     * Applies breathing (length modulation), rotation, or both to the line, starting from the original endpoints.
     * Call this in your animation loop to combine effects without interference.
     *
     * @param {Object} options - { sinusoid, t, degPerSec }
     *   sinusoid: SWSinusoid instance (optional, for breathing)
     *   t: time in seconds
     *   degPerSec: angular velocity (optional, for rotation)
     *
     * Usage:
     *   line.transform({sinusoid, t, degPerSec}); // both
     *   line.transform({sinusoid, t}); // just breathing
     *   line.transform({degPerSec, t}); // just rotation
     */
    transform({sinusoid = null, t = 0, degPerSec = null} = {}) {
        // 1. Start from original endpoints
        const ax0 = this.originalA.x;
        const ay0 = this.originalA.y;
        const bx0 = this.originalB.x;
        const by0 = this.originalB.y;
        // 2. Find original midpoint
        const midX = (ax0 + bx0) / 2;
        const midY = (ay0 + by0) / 2;
        // 3. Vector from midpoint to each endpoint
        let dax = ax0 - midX;
        let day = ay0 - midY;
        let dbx = bx0 - midX;
        let dby = by0 - midY;
        // 4. Breathing: scale vectors if sinusoid is provided
        if (sinusoid) {
            const origLen = Math.sqrt((bx0 - ax0) ** 2 + (by0 - ay0) ** 2);
            const newLen = sinusoid.getValue(t);
            const scale = (origLen === 0) ? 1 : (newLen / origLen);
            dax *= scale;
            day *= scale;
            dbx *= scale;
            dby *= scale;
        }
        // 5. Rotation: rotate vectors if degPerSec is provided
        if (degPerSec !== null) {
            const angleRad = (degPerSec * t) * Math.PI / 180;
            const cosA = Math.cos(angleRad);
            const sinA = Math.sin(angleRad);
            const rotate = (dx, dy) => [dx * cosA - dy * sinA, dx * sinA + dy * cosA];
            [dax, day] = rotate(dax, day);
            [dbx, dby] = rotate(dbx, dby);
        }
        // 6. Set new endpoints
        this.ptA.x = midX + dax;
        this.ptA.y = midY + day;
        this.ptB.x = midX + dbx;
        this.ptB.y = midY + dby;
        // 7. Update midpoint
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        // 8. Update length
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end transform

    /**
     * Rotates the line about a designated endpoint (fixedPt) at a given angular velocity (deg/sec).
     * Usage: line.rotateAbout(line.ptA, degPerSec, t) or line.rotateAbout(line.ptB, degPerSec, t)
     * @param {SWPoint} fixedPt - The endpoint to rotate about (must be ptA or ptB)
     * @param {number} degPerSec - Angular velocity in degrees per second
     * @param {number} t - Time in seconds
     */
    rotateAbout(fixedPt, degPerSec, t) {
        // Determine which endpoint is fixed and which is moving
        let isA = (fixedPt === this.ptA);
        let origFixed = isA ? this.originalA : this.originalB;
        let origMoving = isA ? this.originalB : this.originalA;
        // Calculate angle in radians
        const angleRad = degPerSec * t * Math.PI / 180;
        // Vector from fixed to moving (original positions)
        const dx = origMoving.x - origFixed.x;
        const dy = origMoving.y - origFixed.y;
        // Rotate moving endpoint around fixed
        const newX = origFixed.x + (dx * Math.cos(angleRad) - dy * Math.sin(angleRad));
        const newY = origFixed.y + (dx * Math.sin(angleRad) + dy * Math.cos(angleRad));
        // Set endpoints
        if (isA) {
            this.ptA.x = origFixed.x;
            this.ptA.y = origFixed.y;
            this.ptB.x = newX;
            this.ptB.y = newY;
        } else {
            this.ptB.x = origFixed.x;
            this.ptB.y = origFixed.y;
            this.ptA.x = newX;
            this.ptA.y = newY;
        }
        // Update midpoint and length
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end rotateAbout

    /**
     * Combined transformation: breathing and rotation about a fixed endpoint.
     * Similar to transform() but rotates about a fixed endpoint instead of the midpoint.
     * @param {SWPoint} fixedPt - The endpoint to keep fixed (must be ptA or ptB)
     * @param {Object} params - Transformation parameters
     * @param {SWSinusoid} params.sinusoid - The sinusoid for breathing (optional)
     * @param {number} params.breatheTime - Time parameter for breathing sinusoid (optional)
     * @param {number} params.degPerSec - Angular velocity in degrees/sec (optional)
     * @param {number} params.rotateTime - Time parameter for rotation (optional)
     * 
     * Usage:
     *   line.transformAbout(line.ptA, {sinusoid, breatheTime, degPerSec, rotateTime}); // both
     *   line.transformAbout(line.ptA, {sinusoid, breatheTime}); // just breathing
     *   line.transformAbout(line.ptA, {degPerSec, rotateTime}); // just rotation
     */
    transformAbout(fixedPt, {sinusoid = null, breatheTime = 0, degPerSec = null, rotateTime = 0} = {}) {
        // Determine which endpoint is fixed and which is moving
        const isA = (fixedPt === this.ptA);
        const origFixed = isA ? this.originalA : this.originalB;
        const origMoving = isA ? this.originalB : this.originalA;
        
        // Vector from fixed to moving (original positions)
        let dx = origMoving.x - origFixed.x;
        let dy = origMoving.y - origFixed.y;
        const origLen = Math.sqrt(dx * dx + dy * dy);
        
        if (origLen === 0) return; // Avoid division by zero
        
        // Apply breathing: scale the vector to the new length
        if (sinusoid) {
            const newLen = sinusoid.getValue(breatheTime);
            const scale = newLen / origLen;
            dx *= scale;
            dy *= scale;
        }
        
        // Apply rotation: rotate the vector
        if (degPerSec !== null) {
            const angleRad = (degPerSec * rotateTime) * Math.PI / 180;
            const cosA = Math.cos(angleRad);
            const sinA = Math.sin(angleRad);
            const newDx = dx * cosA - dy * sinA;
            const newDy = dx * sinA + dy * cosA;
            dx = newDx;
            dy = newDy;
        }
        
        // Calculate new position for moving endpoint
        const newX = origFixed.x + dx;
        const newY = origFixed.y + dy;
        
        // Set endpoints (fixed stays at original, moving gets new position)
        if (isA) {
            this.ptA.x = origFixed.x;
            this.ptA.y = origFixed.y;
            this.ptB.x = newX;
            this.ptB.y = newY;
        } else {
            this.ptB.x = origFixed.x;
            this.ptB.y = origFixed.y;
            this.ptA.x = newX;
            this.ptA.y = newY;
        }
        
        // Update midpoint and length
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end transformAbout

}//end SWLine class


// Export for module use (if needed)
// export default SWLine;
