line.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import { Point } from './point';
  2. import { Geometry } from './geometry';
  3. import { Rectangle } from './rectangle';
  4. export class Line extends Geometry {
  5. get center() {
  6. return new Point((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
  7. }
  8. constructor(x1, y1, x2, y2) {
  9. super();
  10. if (typeof x1 === 'number' && typeof y1 === 'number') {
  11. this.start = new Point(x1, y1);
  12. this.end = new Point(x2, y2);
  13. }
  14. else {
  15. this.start = Point.create(x1);
  16. this.end = Point.create(y1);
  17. }
  18. }
  19. getCenter() {
  20. return this.center;
  21. }
  22. /**
  23. * Rounds the line to the given `precision`.
  24. */
  25. round(precision = 0) {
  26. this.start.round(precision);
  27. this.end.round(precision);
  28. return this;
  29. }
  30. translate(tx, ty) {
  31. if (typeof tx === 'number') {
  32. this.start.translate(tx, ty);
  33. this.end.translate(tx, ty);
  34. }
  35. else {
  36. this.start.translate(tx);
  37. this.end.translate(tx);
  38. }
  39. return this;
  40. }
  41. /**
  42. * Rotate the line by `angle` around `origin`.
  43. */
  44. rotate(angle, origin) {
  45. this.start.rotate(angle, origin);
  46. this.end.rotate(angle, origin);
  47. return this;
  48. }
  49. /**
  50. * Scale the line by `sx` and `sy` about the given `origin`. If origin is not
  51. * specified, the line is scaled around `0,0`.
  52. */
  53. scale(sx, sy, origin) {
  54. this.start.scale(sx, sy, origin);
  55. this.end.scale(sx, sy, origin);
  56. return this;
  57. }
  58. /**
  59. * Returns the length of the line.
  60. */
  61. length() {
  62. return Math.sqrt(this.squaredLength());
  63. }
  64. /**
  65. * Useful for distance comparisons in which real length is not necessary
  66. * (saves one `Math.sqrt()` operation).
  67. */
  68. squaredLength() {
  69. const dx = this.start.x - this.end.x;
  70. const dy = this.start.y - this.end.y;
  71. return dx * dx + dy * dy;
  72. }
  73. /**
  74. * Scale the line so that it has the requested length. The start point of
  75. * the line is preserved.
  76. */
  77. setLength(length) {
  78. const total = this.length();
  79. if (!total) {
  80. return this;
  81. }
  82. const scale = length / total;
  83. return this.scale(scale, scale, this.start);
  84. }
  85. parallel(distance) {
  86. const line = this.clone();
  87. if (!line.isDifferentiable()) {
  88. return line;
  89. }
  90. const { start, end } = line;
  91. const eRef = start.clone().rotate(270, end);
  92. const sRef = end.clone().rotate(90, start);
  93. start.move(sRef, distance);
  94. end.move(eRef, distance);
  95. return line;
  96. }
  97. /**
  98. * Returns the vector of the line with length equal to length of the line.
  99. */
  100. vector() {
  101. return new Point(this.end.x - this.start.x, this.end.y - this.start.y);
  102. }
  103. /**
  104. * Returns the angle of incline of the line.
  105. *
  106. * The function returns `NaN` if the start and end endpoints of the line
  107. * both lie at the same coordinates(it is impossible to determine the angle
  108. * of incline of a line that appears to be a point). The
  109. * `line.isDifferentiable()` function may be used in advance to determine
  110. * whether the angle of incline can be computed for a given line.
  111. */
  112. angle() {
  113. const ref = new Point(this.start.x + 1, this.start.y);
  114. return this.start.angleBetween(this.end, ref);
  115. }
  116. /**
  117. * Returns a rectangle that is the bounding box of the line.
  118. */
  119. bbox() {
  120. const left = Math.min(this.start.x, this.end.x);
  121. const top = Math.min(this.start.y, this.end.y);
  122. const right = Math.max(this.start.x, this.end.x);
  123. const bottom = Math.max(this.start.y, this.end.y);
  124. return new Rectangle(left, top, right - left, bottom - top);
  125. }
  126. /**
  127. * Returns the bearing (cardinal direction) of the line.
  128. *
  129. * The return value is one of the following strings:
  130. * 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' and 'N'.
  131. *
  132. * The function returns 'N' if the two endpoints of the line are coincident.
  133. */
  134. bearing() {
  135. return this.start.bearing(this.end);
  136. }
  137. /**
  138. * Returns the point on the line that lies closest to point `p`.
  139. */
  140. closestPoint(p) {
  141. return this.pointAt(this.closestPointNormalizedLength(p));
  142. }
  143. /**
  144. * Returns the length of the line up to the point that lies closest to point `p`.
  145. */
  146. closestPointLength(p) {
  147. return this.closestPointNormalizedLength(p) * this.length();
  148. }
  149. /**
  150. * Returns a line that is tangent to the line at the point that lies closest
  151. * to point `p`.
  152. */
  153. closestPointTangent(p) {
  154. return this.tangentAt(this.closestPointNormalizedLength(p));
  155. }
  156. /**
  157. * Returns the normalized length (distance from the start of the line / total
  158. * line length) of the line up to the point that lies closest to point.
  159. */
  160. closestPointNormalizedLength(p) {
  161. const product = this.vector().dot(new Line(this.start, p).vector());
  162. const normalized = Math.min(1, Math.max(0, product / this.squaredLength()));
  163. // normalized returns `NaN` if this line has zero length
  164. if (Number.isNaN(normalized)) {
  165. return 0;
  166. }
  167. return normalized;
  168. }
  169. /**
  170. * Returns a point on the line that lies `rate` (normalized length) away from
  171. * the beginning of the line.
  172. */
  173. pointAt(ratio) {
  174. const start = this.start;
  175. const end = this.end;
  176. if (ratio <= 0) {
  177. return start.clone();
  178. }
  179. if (ratio >= 1) {
  180. return end.clone();
  181. }
  182. return start.lerp(end, ratio);
  183. }
  184. /**
  185. * Returns a point on the line that lies length away from the beginning of
  186. * the line.
  187. */
  188. pointAtLength(length) {
  189. const start = this.start;
  190. const end = this.end;
  191. let fromStart = true;
  192. if (length < 0) {
  193. fromStart = false; // start calculation from end point
  194. length = -length; // eslint-disable-line
  195. }
  196. const total = this.length();
  197. if (length >= total) {
  198. return fromStart ? end.clone() : start.clone();
  199. }
  200. const rate = (fromStart ? length : total - length) / total;
  201. return this.pointAt(rate);
  202. }
  203. /**
  204. * Divides the line into two lines at the point that lies `rate` (normalized
  205. * length) away from the beginning of the line.
  206. */
  207. divideAt(ratio) {
  208. const dividerPoint = this.pointAt(ratio);
  209. return [
  210. new Line(this.start, dividerPoint),
  211. new Line(dividerPoint, this.end),
  212. ];
  213. }
  214. /**
  215. * Divides the line into two lines at the point that lies length away from
  216. * the beginning of the line.
  217. */
  218. divideAtLength(length) {
  219. const dividerPoint = this.pointAtLength(length);
  220. return [
  221. new Line(this.start, dividerPoint),
  222. new Line(dividerPoint, this.end),
  223. ];
  224. }
  225. /**
  226. * Returns `true` if the point `p` lies on the line. Return `false` otherwise.
  227. */
  228. containsPoint(p) {
  229. const start = this.start;
  230. const end = this.end;
  231. // cross product of 0 indicates that this line and
  232. // the vector to `p` are collinear.
  233. if (start.cross(p, end) !== 0) {
  234. return false;
  235. }
  236. const length = this.length();
  237. if (new Line(start, p).length() > length) {
  238. return false;
  239. }
  240. if (new Line(p, end).length() > length) {
  241. return false;
  242. }
  243. return true;
  244. }
  245. intersect(shape, options) {
  246. const ret = shape.intersectsWithLine(this, options);
  247. if (ret) {
  248. return Array.isArray(ret) ? ret : [ret];
  249. }
  250. return null;
  251. }
  252. /**
  253. * Returns the intersection point of the line with another line. Returns
  254. * `null` if no intersection exists.
  255. */
  256. intersectsWithLine(line) {
  257. const pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y);
  258. const pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y);
  259. const det = pt1Dir.x * pt2Dir.y - pt1Dir.y * pt2Dir.x;
  260. const deltaPt = new Point(line.start.x - this.start.x, line.start.y - this.start.y);
  261. const alpha = deltaPt.x * pt2Dir.y - deltaPt.y * pt2Dir.x;
  262. const beta = deltaPt.x * pt1Dir.y - deltaPt.y * pt1Dir.x;
  263. if (det === 0 || alpha * det < 0 || beta * det < 0) {
  264. return null;
  265. }
  266. if (det > 0) {
  267. if (alpha > det || beta > det) {
  268. return null;
  269. }
  270. }
  271. else if (alpha < det || beta < det) {
  272. return null;
  273. }
  274. return new Point(this.start.x + (alpha * pt1Dir.x) / det, this.start.y + (alpha * pt1Dir.y) / det);
  275. }
  276. /**
  277. * Returns `true` if a tangent line can be found for the line.
  278. *
  279. * Tangents cannot be found if both of the line endpoints are coincident
  280. * (the line appears to be a point).
  281. */
  282. isDifferentiable() {
  283. return !this.start.equals(this.end);
  284. }
  285. /**
  286. * Returns the perpendicular distance between the line and point. The
  287. * distance is positive if the point lies to the right of the line, negative
  288. * if the point lies to the left of the line, and `0` if the point lies on
  289. * the line.
  290. */
  291. pointOffset(p) {
  292. const ref = Point.clone(p);
  293. const start = this.start;
  294. const end = this.end;
  295. const determinant = (end.x - start.x) * (ref.y - start.y) -
  296. (end.y - start.y) * (ref.x - start.x);
  297. return determinant / this.length();
  298. }
  299. pointSquaredDistance(x, y) {
  300. const p = Point.create(x, y);
  301. return this.closestPoint(p).squaredDistance(p);
  302. }
  303. pointDistance(x, y) {
  304. const p = Point.create(x, y);
  305. return this.closestPoint(p).distance(p);
  306. }
  307. /**
  308. * Returns a line tangent to the line at point that lies `rate` (normalized
  309. * length) away from the beginning of the line.
  310. */
  311. tangentAt(ratio) {
  312. if (!this.isDifferentiable()) {
  313. return null;
  314. }
  315. const start = this.start;
  316. const end = this.end;
  317. const tangentStart = this.pointAt(ratio);
  318. const tangentLine = new Line(start, end);
  319. tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y);
  320. return tangentLine;
  321. }
  322. /**
  323. * Returns a line tangent to the line at point that lies `length` away from
  324. * the beginning of the line.
  325. */
  326. tangentAtLength(length) {
  327. if (!this.isDifferentiable()) {
  328. return null;
  329. }
  330. const start = this.start;
  331. const end = this.end;
  332. const tangentStart = this.pointAtLength(length);
  333. const tangentLine = new Line(start, end);
  334. tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y);
  335. return tangentLine;
  336. }
  337. relativeCcw(x, y) {
  338. const ref = Point.create(x, y);
  339. let dx1 = ref.x - this.start.x;
  340. let dy1 = ref.y - this.start.y;
  341. const dx2 = this.end.x - this.start.x;
  342. const dy2 = this.end.y - this.start.y;
  343. let ccw = dx1 * dy2 - dy1 * dx2;
  344. if (ccw === 0) {
  345. ccw = dx1 * dx2 + dy1 * dy2;
  346. if (ccw > 0.0) {
  347. dx1 -= dx2;
  348. dy1 -= dy2;
  349. ccw = dx1 * dx2 + dy1 * dy2;
  350. if (ccw < 0.0) {
  351. ccw = 0.0;
  352. }
  353. }
  354. }
  355. return ccw < 0.0 ? -1 : ccw > 0.0 ? 1 : 0;
  356. }
  357. /**
  358. * Return `true` if the line equals the other line.
  359. */
  360. equals(l) {
  361. return (l != null &&
  362. this.start.x === l.start.x &&
  363. this.start.y === l.start.y &&
  364. this.end.x === l.end.x &&
  365. this.end.y === l.end.y);
  366. }
  367. /**
  368. * Returns another line which is a clone of the line.
  369. */
  370. clone() {
  371. return new Line(this.start, this.end);
  372. }
  373. toJSON() {
  374. return { start: this.start.toJSON(), end: this.end.toJSON() };
  375. }
  376. serialize() {
  377. return [this.start.serialize(), this.end.serialize()].join(' ');
  378. }
  379. }
  380. (function (Line) {
  381. function isLine(instance) {
  382. return instance != null && instance instanceof Line;
  383. }
  384. Line.isLine = isLine;
  385. })(Line || (Line = {}));
  386. //# sourceMappingURL=line.js.map