• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23978164017

04 Apr 2026 11:41AM UTC coverage: 74.15% (+0.2%) from 73.912%
23978164017

push

github

web-flow
Experimental 3D text example (#2471)

5153 of 7858 branches covered (65.58%)

Branch coverage included in aggregate %.

463 of 552 new or added lines in 9 files covered. (83.88%)

11642 of 14792 relevant lines covered (78.7%)

747.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.2
/modules/text/src/paths/shape-path.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4
// Adapted from THREE.js ShapePath (https://github.com/mrdoob/three.js/) under the MIT License.
5

6
import {Vector2} from '@math.gl/core';
7
import {Path, Shape} from './path';
8
import {ShapeUtils} from './shape-utils';
9

10
/** Collects sub-paths that form a single glyph outline. */
11
export class ShapePath {
12
  /** All sub-paths that compose the shape. */
13
  subPaths: Path[] = [];
13✔
14
  /** The path currently receiving drawing commands. */
15
  currentPath: Path | null = null;
13✔
16

17
  /** Starts a new sub-path at the provided coordinates. */
18
  moveTo(x: number, y: number): this {
19
    this.currentPath = new Path();
26✔
20
    this.subPaths.push(this.currentPath);
26✔
21
    this.currentPath.moveTo(x, y);
26✔
22
    return this;
26✔
23
  }
24

25
  /** Draws a straight line from the current point. */
26
  lineTo(x: number, y: number): this {
27
    this.currentPath?.lineTo(x, y);
168✔
28
    return this;
168✔
29
  }
30

31
  /** Draws a quadratic curve from the current point. */
32
  quadraticCurveTo(controlPointX: number, controlPointY: number, x: number, y: number): this {
NEW
33
    this.currentPath?.quadraticCurveTo(controlPointX, controlPointY, x, y);
×
NEW
34
    return this;
×
35
  }
36

37
  /** Draws a cubic bezier curve from the current point. */
38
  // eslint-disable-next-line max-params
39
  bezierCurveTo(
40
    controlPoint1X: number,
41
    controlPoint1Y: number,
42
    controlPoint2X: number,
43
    controlPoint2Y: number,
44
    x: number,
45
    y: number
46
  ): this {
NEW
47
    this.currentPath?.bezierCurveTo(
×
48
      controlPoint1X,
49
      controlPoint1Y,
50
      controlPoint2X,
51
      controlPoint2Y,
52
      x,
53
      y
54
    );
NEW
55
    return this;
×
56
  }
57

58
  /** Draws a spline passing through the provided points. */
59
  splineThru(points: Vector2[]): this {
NEW
60
    this.currentPath?.splineThru(points);
×
NEW
61
    return this;
×
62
  }
63

64
  /** Converts the set of sub-paths into closed shapes and holes. */
65
  toShapes(isCounterClockWise?: boolean): Shape[] {
66
    const subPaths = this.subPaths;
7✔
67
    if (subPaths.length === 0) {
7!
NEW
68
      return [];
×
69
    }
70

71
    if (subPaths.length === 1) {
7!
NEW
72
      const shape = new Shape();
×
NEW
73
      shape.curves = subPaths[0].curves;
×
NEW
74
      return [shape];
×
75
    }
76

77
    const outerIsClockWise = !(isCounterClockWise ?? false);
7✔
78

79
    const rings = subPaths
7✔
80
      .map(path => {
81
        const points = closePath(path.getPoints());
14✔
82
        return {path, points, area: Math.abs(ShapeUtils.area(points))};
14✔
83
      })
84
      .sort((firstRing, secondRing) => secondRing.area - firstRing.area);
7✔
85

86
    const shapes: Shape[] = [];
7✔
87
    const boundaries: Vector2[][] = [];
7✔
88

89
    for (const ring of rings) {
7✔
90
      if (ring.points.length > 0) {
14!
91
        const containerIndex = boundaries.findIndex(boundary =>
14✔
92
          isPointInsidePolygon(ring.points[0], boundary)
7✔
93
        );
94
        if (containerIndex >= 0) {
14✔
95
          const containerShape = shapes[containerIndex];
7✔
96
          containerShape.holes.push(ring.path);
7✔
97
        } else {
98
          const shape = new Shape();
7✔
99
          shape.curves = ring.path.curves;
7✔
100
          shapes.push(shape);
7✔
101
          const orientedBoundary =
102
            ShapeUtils.isClockWise(ring.points) === outerIsClockWise
7✔
103
              ? ring.points
104
              : ring.points.slice().reverse();
105
          boundaries.push(orientedBoundary);
7✔
106
        }
107
      }
108
    }
109

110
    return shapes;
7✔
111
  }
112
}
113

114
/** Returns a closed copy of the provided polygon point list. */
115
function closePath(points: Vector2[]): Vector2[] {
116
  if (points.length === 0) {
14!
NEW
117
    return points;
×
118
  }
119

120
  const firstPoint = points[0];
14✔
121
  const lastPoint = points[points.length - 1];
14✔
122
  if (firstPoint.equals(lastPoint)) {
14!
123
    return points;
14✔
124
  }
125

NEW
126
  return [...points, firstPoint.clone()];
×
127
}
128

129
/** Determines whether a point lies inside a polygon. */
130
function isPointInsidePolygon(point: Vector2, polygon: Vector2[]): boolean {
131
  const polygonLength = polygon.length;
7✔
132
  let isInside = false;
7✔
133

134
  for (
7✔
135
    let current = 0, previous = polygonLength - 1;
7✔
136
    current < polygonLength;
137
    previous = current++
138
  ) {
139
    const lowerPoint = polygon[previous];
727✔
140
    const higherPoint = polygon[current];
727✔
141
    const edgeDeltaX = higherPoint.x - lowerPoint.x;
727✔
142
    const edgeDeltaY = higherPoint.y - lowerPoint.y;
727✔
143

144
    if (Math.abs(edgeDeltaY) <= Number.EPSILON) {
727✔
145
      if (point.y === lowerPoint.y) {
367!
146
        const withinXBand =
NEW
147
          (higherPoint.x <= point.x && point.x <= lowerPoint.x) ||
×
148
          (lowerPoint.x <= point.x && point.x <= higherPoint.x);
NEW
149
        if (withinXBand) {
×
NEW
150
          return true;
×
151
        }
152
      }
153
    } else {
154
      const adjustedLower = edgeDeltaY < 0 ? higherPoint : lowerPoint;
360✔
155
      const adjustedHigher = edgeDeltaY < 0 ? lowerPoint : higherPoint;
360✔
156
      const adjustedDeltaX = edgeDeltaY < 0 ? -edgeDeltaX : edgeDeltaX;
360✔
157
      const adjustedDeltaY = edgeDeltaY < 0 ? -edgeDeltaY : edgeDeltaY;
360✔
158

159
      const withinBand = point.y >= adjustedLower.y && point.y <= adjustedHigher.y;
360✔
160
      const alignedWithLower = point.y === adjustedLower.y;
360✔
161
      if (withinBand && alignedWithLower && point.x === adjustedLower.x) {
360!
NEW
162
        return true;
×
163
      }
164
      if (withinBand && !alignedWithLower) {
360✔
165
        const perpendicularEdge =
166
          adjustedDeltaY * (point.x - adjustedLower.x) -
14✔
167
          adjustedDeltaX * (point.y - adjustedLower.y);
168
        if (perpendicularEdge === 0) {
14!
NEW
169
          return true;
×
170
        }
171
        if (perpendicularEdge > 0) {
14✔
172
          isInside = !isInside;
7✔
173
        }
174
      }
175
    }
176
  }
177

178
  return isInside;
7✔
179
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc