forked from rabbit-ear/rabbit-ear-app
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsnap.js
154 lines (151 loc) · 5.18 KB
/
snap.js
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
144
145
146
147
148
149
150
151
152
153
154
import {
distance2,
subtract2,
} from "rabbit-ear/math/vector.js";
import {
clampSegment,
} from "rabbit-ear/math/line.js";
import {
nearestPointOnLine,
} from "rabbit-ear/math/nearest.js";
import {
overlapLinePoint,
} from "rabbit-ear/math/overlap.js";
import {
nearestVertex,
nearestEdge,
nearestFace,
} from "rabbit-ear/graph/nearest.js";
const _0_866 = Math.sqrt(3) / 2;
export const triangleGridSnapFunction = (point, snapRadius) => {
if (!point) { return undefined; }
const yCoordCount = Math.round(point[1] / _0_866);
const yCoord = yCoordCount * _0_866;
const yRemainder = Math.abs(point[1] - yCoord);
const xOffset = yCoordCount % 2 === 0 ? 0 : 0.5;
const xCoord = Math.round(point[0] + xOffset) - xOffset;
const xRemainder = Math.abs(point[0] - xCoord);
return xRemainder < snapRadius && yRemainder < snapRadius
? [xCoord, yCoord]
: undefined;
};
export const squareGridSnapFunction = (point, snapRadius) => {
if (!point) { return undefined; }
const coords = point.map(n => Math.round(n));
const isNear = point
.map((n, i) => Math.abs(coords[i] - n))
.map(d => d < snapRadius)
.reduce((a, b) => a && b, true);
return isNear ? coords : undefined
};
/**
* @description Snap a point to either one point from a list of points
* or to a grid-line point if either is within the range specified
* by a snap radius.
* @param {number[]} point the point we want to snap
* @param {number[][]} points a list of snap points to test against
* @param {number} snapRadius the epsilon range, any points outside
* this will be ignored.
* @returns {object} object with coords {number[]} and snap {boolean}
*/
export const snapToPoint = (
point,
points,
snapRadius,
gridSnapFunction = squareGridSnapFunction) => {
// console.log("snapToPoint", point, points, snapRadius);
if (!point) { return { coords: undefined, snap: false }; }
if (!points || !points.length) {
const grid = gridSnapFunction(point, snapRadius);
return grid !== undefined
? { coords: grid, snap: true }
: { coords: point, snap: false };
}
// these points take priority over grid points.
const pointsDistance = points.map(p => distance2(p, point));
const nearestPointIndex = pointsDistance
.map((d, i) => d < snapRadius ? i : undefined)
.filter(a => a !== undefined)
.sort((a, b) => pointsDistance[a] - pointsDistance[b])
.shift();
// if a point exists within our snap radius, use that
if (nearestPointIndex !== undefined) {
return { coords: [...points[nearestPointIndex]], snap: true };
}
// fallback, use a grid point if it exists.
// we only need the nearest of the grid coordinates.
const grid = gridSnapFunction(point, snapRadius);
if (grid !== undefined) {
return { coords: grid, snap: true };
}
// fallback, return the input point.
return { coords: [...point], snap: false };
};
/**
* @param {number[]} point
* @param {number[][]} points
* @param { line: VecLine, clamp: function, domain: function } rulers
* @param {number} snapRadius
*/
export const snapToRulerLine = (point, points, rulers, snapRadius) => {
// console.log("snapToRulerLine", point, points, rulers, snapRadius);
if (!point) {
return { coords: undefined, snap: false };
}
if (!rulers || !rulers.length) {
return { coords: point, snap: false };
}
// for each ruler, a point that is the nearest point on the line
const rulersPoint = rulers
.map(el => nearestPointOnLine(el.line, point, el.clamp));
// for each ruler point, the distance to our input point
const distances = rulersPoint.map(p => distance2(point, p));
// find the nearest point
let index = 0;
for (let i = 1; i < distances.length; i += 1) {
if (distances[i] < distances[index]) { index = i; }
}
const ruler = rulers[index];
const rulerPoint = rulersPoint[index];
// even if our goal is simply to snap to a ruler line, there may be a
// snap point that lies along the nearest snapping ruler.
// it's a snap within a snap behavior which, once you see, UX-wise,
// it's a behavior that a user would expect to receive.
// Now that we have found the nearest snap line, this is a subset of
// snapPoints which overlap this snap line.
const collinearSnapPoints = points
.filter(p => overlapLinePoint(ruler.line, p, ruler.domain));
const snapPoint = snapToPoint(rulerPoint, collinearSnapPoints, snapRadius);
return snapPoint.snap
? snapPoint
: { coords: rulerPoint, snap: true };
};
/**
* @param {number[]} point
* @param {FOLD} graph a FOLD graph with vertices_coords, edges_vertices
* @param {number} snapRadius
*/
export const snapToEdge = (point, graph, snapRadius) => {
if (!point || !graph || !graph.vertices_coords || !graph.edges_vertices) {
return { snap: false, edge: undefined, coords: undefined };
}
let edge;
try {
edge = nearestEdge(graph, point);
} catch (error) {
edge = undefined;
}
if (edge === undefined) {
return { snap: false, edge: undefined, coords: point};
}
const seg = graph.edges_vertices[edge].map(v => graph.vertices_coords[v]);
const nearestPoint = nearestPointOnLine(
{ vector: subtract2(seg[1], seg[0]), origin: seg[0] },
point,
clampSegment,
);
const distance = distance2(point, nearestPoint);
return distance < snapRadius
? { snap: true, edge, coords: nearestPoint }
: { snap: false, edge: undefined, coords: point };
};