@kylebarron/snap-to-tin
Version:
Snap vector features to the faces of a triangulated irregular network (TIN).
161 lines (160 loc) • 6.06 kB
JavaScript
export function interpolateTriangle(point, triangle) {
const az = triangle[2];
const bz = triangle[5];
const cz = triangle[8];
// Find the mix of a, b, and c to use
const mix = barycentric2d(point, triangle);
// If point is outside triangle, return null
if (mix[0] < 0 ||
1 < mix[0] ||
mix[1] < 0 ||
1 < mix[1] ||
mix[2] < 0 ||
1 < mix[2]) {
return null;
}
// Find the correct z based on that mix
const interpolatedZ = mix[0] * az + mix[1] * bz + mix[2] * cz;
return [point[0], point[1], interpolatedZ];
}
// Interpolate when point is known to be on triangle edge
// Can be much faster than working with barycentric coordinates
export function interpolateEdge(triangle, point) {
// loop over each edge until you find one where the point is on the line
for (const edge of triangleToEdges(triangle)) {
const start = edge[0];
const end = edge[1];
const onLine = pointOnLine2d(start, end, point);
if (!onLine)
continue;
// percent distance from start to end
const pctAlong = distanceLine2d(start, point) / distanceLine2d(start, end);
const z = start[2] + pctAlong * (end[2] - start[2]);
return [point[0], point[1], z];
}
return null;
}
// https://stackoverflow.com/a/11912171
export function pointOnLine2d(a, b, point) {
return floatIsClose(distanceLine2d(a, point) + distanceLine2d(b, point) - distanceLine2d(a, b), 0);
}
export function distanceLine2d(a, b) {
const dx = b[0] - a[0];
const dy = b[1] - a[1];
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
export function floatIsClose(a, b, eps = 1e-10) {
return Math.abs(a - b) < eps;
}
// Modfied slightly from https://stackoverflow.com/a/24392281
// returns intersection point if the line from a->b intersects with c->d
// Otherwise returns false
export function lineLineIntersection2d(a, b, c, d) {
// ∆x1 * ∆y2 - ∆x2 * ∆y1
const det = (b[0] - a[0]) * (d[1] - c[1]) - (d[0] - c[0]) * (b[1] - a[1]);
if (det === 0) {
// NOTE: lines are parallel
return null;
}
// pct distance along each line
const lambda = ((d[1] - c[1]) * (d[0] - a[0]) + (c[0] - d[0]) * (d[1] - a[1])) / det;
const gamma = ((a[1] - b[1]) * (d[0] - a[0]) + (b[0] - a[0]) * (d[1] - a[1])) / det;
if (!(0 <= lambda && lambda <= 1 && 0 <= gamma && gamma <= 1)) {
// intersects outside the line segments
return null;
}
// With the current implementation, lambda is correctly the percent distance along the first line
// from a to b, but gamma is the percent distance **back** from d to c It isn't worth my time to
// figure out how to change the function, but just keep that in mind.
// Find intersection point
// Use lambda for pct along a-b
const x = a[0] + lambda * (b[0] - a[0]);
const y = a[1] + lambda * (b[1] - a[1]);
return [x, y];
}
// Test line-line intersection among line and each edge of the triangle
export function lineTriangleIntersect2d(line, triangle) {
// loop over each edge
const intersectionPoints = [];
for (const edge of triangleToEdges(triangle)) {
const intersectionPoint = lineLineIntersection2d(line[0], line[1], edge[0], edge[1]);
if (intersectionPoint) {
intersectionPoints.push(intersectionPoint);
}
}
return intersectionPoints;
}
export function* triangleToEdges(triangle) {
for (let i = 0; i < 3; i++) {
let edge = [];
if (i === 0) {
edge.push(triangleVertex(0, triangle));
edge.push(triangleVertex(1, triangle));
}
else if (i === 1) {
edge.push(triangleVertex(1, triangle));
edge.push(triangleVertex(2, triangle));
}
else if (i === 2) {
edge.push(triangleVertex(2, triangle));
edge.push(triangleVertex(0, triangle));
}
yield edge;
}
}
export function triangleVertex(i, triangle) {
return triangle.subarray(i * 3, (i + 1) * 3);
}
// Split line into desired number of segments
export function splitLine2d(line, nSegments) {
const [start, end] = line;
const lineSegments = [];
for (let i = 0; i < nSegments; i++) {
// _i_th part of the way from min to max
const a = start[0] + (i / nSegments) * (end[0] - start[0]);
const b = start[1] + (i / nSegments) * (end[1] - start[1]);
const c = start[0] + ((i + 1) / nSegments) * (end[0] - start[0]);
const d = start[1] + ((i + 1) / nSegments) * (end[1] - start[1]);
lineSegments.push([
[a, b],
[c, d]
]);
}
return lineSegments;
}
export function triangleToBounds(triangle) {
if (triangle.length !== 9) {
throw new Error(`Incorrect length of triangle: ${triangle.length}`);
}
const minX = Math.min(triangle[0], triangle[3], triangle[6]);
const maxX = Math.max(triangle[0], triangle[3], triangle[6]);
const minY = Math.min(triangle[1], triangle[4], triangle[7]);
const maxY = Math.max(triangle[1], triangle[4], triangle[7]);
return [minX, minY, maxX, maxY];
}
export function pointInTriangle2d(p, triangle) {
const [x, y, z] = barycentric2d(p, triangle);
return x >= 0 && y >= 0 && z >= 0;
}
// From https://stackoverflow.com/a/14382692
export function barycentric2d(p, triangle) {
const p0 = triangle.subarray(0, 3);
const p1 = triangle.subarray(3, 6);
const p2 = triangle.subarray(6, 9);
const area = 0.5 *
(-p1[1] * p2[0] +
p0[1] * (-p1[0] + p2[0]) +
p0[0] * (p1[1] - p2[1]) +
p1[0] * p2[1]);
const s = (1 / (2 * area)) *
(p0[1] * p2[0] -
p0[0] * p2[1] +
(p2[1] - p0[1]) * p[0] +
(p0[0] - p2[0]) * p[1]);
const t = (1 / (2 * area)) *
(p0[0] * p1[1] -
p0[1] * p1[0] +
(p0[1] - p1[1]) * p[0] +
(p1[0] - p0[0]) * p[1]);
return [1 - s - t, s, t];
}