All files / src/elements/sector AngleMarkerElement.ts

96.5% Statements 138/143
94.44% Branches 17/18
100% Functions 10/10
96.5% Lines 138/143

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 1441x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 16x 16x 1x 1x 1x 1x 1x 1x 1x 45x 45x 45x 1x 1x 43x 43x 1x 1x 1x 1x 22x 22x 1x 1x 91x 91x 91x 91x 91x 91x 91x 91x 91x 1x 1x 1x 1x 142x 142x 142x 142x 142x           142x 142x 142x 142x 1x 1x 40x 40x 40x 40x 40x 40x 40x 40x 40x 40x 31x 31x 31x 40x 1x  
/*----------------------------------------------------------------------+
|    Title:	AngleMarkerElement.ts                                       |
|    Part of the geomlib port of David E. Joyce's Geometry Applet.       |
|    TypeScript: 2026, Nelson Brown, brownnrl@gmail.com                  |
+----------------------------------------------------------------------*/
 
import {SectorElement} from "./SectorElement";
import {PointElement} from "../point/PointElement";
import {PlaneElement} from "../plane/PlaneElement";
 
// A small sector drawn at a vertex to mark the interior angle between
// two arms. Unlike a plain `sector;sector` (whose radius is the
// vertex-to-arm distance, so it swings with arm length), an angle
// marker computes its OWN fixed radius — a sensible px default,
// clamped to a fraction of the shorter arm so it never overshoots.
// It also auto-orients to the interior arc, removing the per-marker
// arm-order sign-check the old midpoint-chain pattern needed (#91).
//
// The vertex (_Center) and the two arm points are real slate elements
// (drags follow them automatically). _A / _B are INTERNAL points,
// recomputed each update() at the marker radius along each arm — they
// are never registered on the slate. The inherited SectorElement
// drawEdge / drawFace then render the wedge, with drawProgress,
// emphasisAmount, and faceAlpha all working unchanged.
export const DEFAULT_ANGLE_MARKER_RADIUS_PX = 22;
export const ANGLE_MARKER_MAX_ARM_FRACTION = 0.45;
export const ANGLE_MARKER_RING_STEP = 9;
 
export class AngleMarkerElement extends SectorElement {
 
    private _arm1 : PointElement;
    private _arm2 : PointElement;
    private _radiusOverride : number;
 
    // Concentric-ring index for same-vertex markers. 0.8.1 will assign
    // this automatically so overlapping markers nest; for now it
    // defaults to 0 and can be set by hand. Each step bumps the radius
    // by ANGLE_MARKER_RING_STEP.
    public ringIndex : number = 0;
 
    // Draw the major (reflex, > 180°) arc instead of the interior
    // minor one. Default false — interior is the faithful Euclidean
    // case (his angles live in 0..180°). Set via the
    // `angleMarkerReflex` construction for the rare reflex marking
    // (e.g. a III.20-style central angle).
    public reflex : boolean = false;
 
    constructor(vertex: PointElement, arm1: PointElement, arm2: PointElement,
                plane: PlaneElement, radiusOverride?: number, reflex?: boolean) {
        super();
        this.dimension = 2;
        this._Center = vertex;
        this._arm1 = arm1;
        this._arm2 = arm2;
        this._P = plane;
        this._radiusOverride = radiusOverride != null ? radiusOverride : null;
        this.reflex = reflex === true;
        // Internal arc endpoints — placed in update().
        this._A = new PointElement();
        this._B = new PointElement();
    }
 
    // The vertex this marker sits at — used to group same-vertex
    // markers for auto radius-stepping (#103).
    get vertex() : PointElement { return this._Center; }
 
    // Whether the author pinned an explicit radius (opts the marker out
    // of auto radius-stepping).
    get hasRadiusOverride() : boolean { return this._radiusOverride != null; }
 
    // The interior angle magnitude (radians) between the two arms,
    // computed directly from the arm points so it's available before
    // update() places _A/_B. Used to order a same-vertex group
    // (smallest angle innermost).
    spanRadians() : number {
        return Math.abs(this._Center.angle(this._arm1, this._arm2, this._P));
    }
 
    // update() orders _A/_B so the inherited minor angle is negative
    // (interior, drawn with the default anticlockwise=true). A reflex
    // marker draws the MAJOR arc between the same two rays — same
    // endpoints, opposite side — which is the magnitude 2π−|minor| with
    // the anticlockwise flag flipped (see _anticlockwise).
    protected _arcAngle(): number {
        const minor = this._Center.angle(this._A, this._B, this._P);
        return this.reflex ? (2 * Math.PI - Math.abs(minor)) : minor;
    }
 
    protected _anticlockwise(): boolean {
        return !this.reflex;
    }
 
    // Angle markers flash their whole wedge during a transition (the
    // emphasis bump), not just the edge.
    protected _flashFace(): boolean {
        return true;
    }
 
    radius() : number {
        const a1 = this._Center.distance(this._arm1);
        const a2 = this._Center.distance(this._arm2);
        const shorter = Math.min(a1, a2);
        const base = this._radiusOverride != null
            ? this._radiusOverride
            : DEFAULT_ANGLE_MARKER_RADIUS_PX;
        const clamped = Math.min(base, shorter * ANGLE_MARKER_MAX_ARM_FRACTION);
        return clamped + this.ringIndex * ANGLE_MARKER_RING_STEP;
    }
 
    // Place an internal endpoint at distance r from the vertex along
    // the direction of `arm`.
    private _place(target: PointElement, arm: PointElement, r: number) : void {
        const dx = arm.x - this._Center.x;
        const dy = arm.y - this._Center.y;
        const dz = arm.z - this._Center.z;
        const len = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (len === 0) {
            target.x = this._Center.x;
            target.y = this._Center.y;
            target.z = this._Center.z;
            return;
        }
        target.x = this._Center.x + dx / len * r;
        target.y = this._Center.y + dy / len * r;
        target.z = this._Center.z + dz / len * r;
    }
 
    update() : void {
        const r = this.radius();
        this._place(this._A, this._arm1, r);
        this._place(this._B, this._arm2, r);
        // Auto-interior: PointElement.angle is signed (−π..π) and atan2
        // already returns the short way, so the magnitude is the
        // interior angle; only the rendered sweep direction matters.
        // The empirically-interior case (matching the old sign-checks)
        // is angle(_A, _B) < 0 — if it's positive, swap the arms.
        const ang = this._Center.angle(this._A, this._B, this._P);
        if (ang > 0) {
            this._place(this._A, this._arm2, r);
            this._place(this._B, this._arm1, r);
        }
    }
}