1 /*
2     Copyright © 2022, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4 
5     Author: Asahi Lina
6 */
7 module creator.viewport.common.spline;
8 import creator.viewport.common.mesh;
9 import creator.viewport;
10 import inochi2d;
11 import inochi2d.core.dbg;
12 import bindbc.opengl;
13 import std.algorithm.mutation;
14 import std.array;
15 import std.math : isFinite;
16 import std.stdio;
17 
18 private {
19     float nearestLine(vec2 a, vec2 b, vec2 c)
20     {
21         return ((c.x - a.x) * (b.x - a.x) + (c.y - a.y) * (b.y - a.y)) / ((b.x - a.x) ^^ 2 + (b.y - a.y) ^^ 2);
22     }
23 
24     BezierSegment catmullToBezier(SplinePoint *p0, SplinePoint *p1, SplinePoint *p2, SplinePoint *p3, float tension = 1.0) {
25         BezierSegment res;
26 
27         res.p[0] = p1.position;
28         res.p[1] = p1.position + (p2.position - p0.position) / (6 * tension);
29         res.p[2] = p2.position - (p3.position - p1.position) / (6 * tension);
30         res.p[3] = p2.position;
31 
32         return res;
33     }
34 }
35 
36 struct SplinePoint {
37     vec2 position;
38     float weightL;
39     float weightR;
40 }
41 
42 struct BezierSegment {
43     vec2[4] p;
44 
45     void split(float t, out BezierSegment left, out BezierSegment right) {
46         float s = 1 - t;
47 
48         vec2 mc = p[1] * s + p[2] * t;
49         vec2 a1 = p[0] * s + p[1] * t;
50         vec2 a2 = a1 * s + mc * t;
51         vec2 b2 = p[2] * s + p[3] * t;
52         vec2 b1 = b2 * t + mc * s;
53         vec2 m = a2 * s + b1 * t;
54 
55         left = BezierSegment([p[0], a1, a2, m]);
56         right = BezierSegment([m, b1, b2, p[3]]);
57     }
58 
59     vec2 eval(float t) {
60         BezierSegment left, right;
61         split(t, left, right);
62         return left.p[3];
63     }
64 }
65 
66 class CatmullSpline {
67 private:
68     vec2[] interpolated;
69     vec3[] drawLines;
70     vec3[] drawPoints;
71     vec2[] refMesh;
72     vec3[] refOffsets;
73 
74 public:
75     uint resolution = 40;
76     float selectRadius = 16f;
77     SplinePoint[] points;
78     CatmullSpline target;
79 
80     void createTarget(IncMesh reference) {
81         target = new CatmullSpline;
82         target.resolution = resolution;
83         target.selectRadius = selectRadius;
84         target.points = points.dup;
85         target.interpolate();
86 
87         refMesh.length = 0;
88         foreach(ref MeshVertex* vtx; reference.vertices) {
89             refMesh ~= vtx.position;
90         }
91         mapReference();
92     }
93 
94     void mapReference() {
95         if (points.length < 2) {
96             refOffsets.length = 0;
97             return;
98         }
99 //         writeln("mapReference");
100         refOffsets.length = 0;
101 
102         float epsilon = 0.0001;
103         foreach(vtx; refMesh) {
104             float off = findClosestPointOffset(vtx);
105             // FIXME: calculate tangent properly
106             vec2 pt = target.eval(off);
107             vec2 pt2 = target.eval(off + epsilon);
108             vec2 tangent = pt2 - pt;
109             tangent.normalize();
110 
111             // FIXME: extrapolation...
112             if (off <= 0 || off >= (points.length - 1)) {
113                 refOffsets ~= vec3(0, 0, float());
114                 continue;
115             }
116             vtx = vtx - pt;
117             vec3 rel = vec3(
118                 vtx.x * tangent.x + vtx.y * tangent.y,
119                 vtx.y * tangent.x - vtx.x * tangent.y,
120                 off,
121             );
122 //             writefln("%s %s %s", vtx, rel, tangent);
123             refOffsets ~= rel;
124         }
125     }
126 
127     void resetTarget(IncMesh mesh) {
128         foreach(i, vtx; refMesh) {
129             mesh.vertices[i].position = vtx;
130         }
131     }
132 
133     void updateTarget(IncMesh mesh) {
134         if (points.length < 2) {
135             resetTarget(mesh);
136             return;
137         }
138 
139         float epsilon = 0.0001;
140         foreach(i, rel; refOffsets) {
141             if (!isFinite(rel.z)) continue;
142 
143             // FIXME: calculate tangent properly
144             vec2 pt = target.eval(rel.z);
145             vec2 pt2 = target.eval(rel.z + epsilon);
146             vec2 tangent = pt2 - pt;
147             tangent = tangent / abs(tangent.distance(vec2(0, 0)));
148 
149             vec2 vtx = vec2(
150                 pt.x + rel.x * tangent.x - rel.y * tangent.y,
151                 pt.y + rel.y * tangent.x + rel.x * tangent.y
152             );
153 //             writefln("%s %s %s", vtx, rel, tangent);
154             mesh.vertices[i].position = vtx;
155         }
156     }
157 
158     void update() {
159         interpolate();
160     }
161 
162     void interpolate() {
163         interpolated.length = 0;
164         drawLines.length = 0;
165 
166         if (points.length == 0) return;
167 
168         vec2 last;
169         foreach(pos; 0..(resolution * (points.length - 1) + 1)) {
170             vec2 p = eval(pos / cast(float)(resolution));
171             interpolated ~= p;
172             if (pos > 0) drawLines ~= [vec3(last.x, last.y, 0), vec3(p.x, p.y, 0)];
173             last = p;
174         }
175 
176         drawPoints.length = 0;
177         foreach(p; points) {
178             drawPoints ~= vec3(p.position.x, p.position.y, 0);
179         }
180     }
181 
182     vec2 eval(float off) {
183         if (off <= 0) return points[0].position;
184         if (off >= (points.length - 1)) return points[$ - 1].position;
185 
186         uint ioff = cast(uint)off;
187         float t = off - ioff;
188         float s = 1 - t;
189 
190         SplinePoint *p1 = &points[ioff];
191         SplinePoint *p2 = &points[ioff + 1];
192 
193         // Two points, linear
194         if (points.length == 2) return p1.position * s + p2.position * t;
195 
196         vec2 evalEdge(vec2 p0, vec2 p1, vec2 p2, float t) {
197             float t2 = t ^^ 2;
198             float t3 = t ^^ 3;
199 
200             float h00 = 2 * t3 - 3 * t2 + 1;
201             float h10 = t3 - 2 * t2 + t;
202             float h01 = -2 * t3 + 3 * t2;
203             float h11 = t3 - t2;
204 
205             vec2 slope = (p2 - p0) / 2;
206             return h11 * slope + h01 * p1 + h00 * p0;
207         }
208 
209         // Edge segment, do the hermite spline thing
210         if (ioff == 0)
211             return evalEdge(p1.position, p2.position, points[ioff + 2].position, t);
212         else if (ioff == points.length - 2)
213             return evalEdge(p2.position, p1.position, points[ioff - 1].position, s);
214 
215         // Middle segment, do Catmull-Rom
216         SplinePoint *p0 = &points[ioff - 1];
217         SplinePoint *p3 = &points[ioff + 2];
218 
219         // By first converting it to Bezier
220         float tension = 1;
221         BezierSegment bezier = catmullToBezier(p0, p1, p2, p3);
222         return bezier.eval(t);
223     }
224 
225     float findClosestPointOffset(vec2 point) {
226         vec2 tangent;
227         return findClosestPointOffset(point, tangent);
228     }
229 
230     float findClosestPointOffset(vec2 point, out vec2 tangent) {
231         tangent = vec2(0, 0);
232 
233         if (points.length == 0) return 0;
234         if (points.length == 1) {
235             return 0;
236         }
237 
238         uint bestIdx = 0;
239         float bestDist = float.infinity;
240         // Find closest interpolated point
241         foreach(pos; 0..(resolution * (points.length - 1) + 1)) {
242             float dist = interpolated[pos].distance(point);
243             if (dist < bestDist) {
244                 bestDist = dist;
245                 bestIdx = cast(uint)pos;
246             }
247         }
248 //         writefln("best %s %s", bestIdx, bestDist);
249 
250         float left = max(0, (bestIdx - 1) / cast(float)resolution);
251         float right = min(points.length - 1, (bestIdx + 1) / cast(float)resolution);
252         vec2 leftPoint = eval(left);
253         vec2 rightPoint = eval(right);
254         float leftDist = abs(point.distance(leftPoint));
255         float rightDist = abs(point.distance(rightPoint));
256 
257         float epsilon = 0.0001;
258 
259         while ((right - left) > epsilon * 2) {
260 //             writefln("left %s right %s", left, right);
261             float mid = (left + right) / 2;
262             vec2 midLPoint = eval(mid - epsilon);
263             vec2 midRPoint = eval(mid + epsilon);
264             if (abs(point.distance(midLPoint)) < abs(point.distance(midRPoint))) {
265                 right = mid;
266                 rightPoint = eval(right);
267                 rightDist = abs(point.distance(rightPoint));
268             } else {
269                 left = mid;
270                 leftPoint = eval(left);
271                 leftDist = abs(point.distance(leftPoint));
272             }
273         }
274         tangent = rightPoint - leftPoint;
275         tangent = tangent / abs(tangent.distance(vec2(0, 0)));
276 
277         if (left == 0) return 0;
278         if (right == points.length - 1) return points.length - 1;
279 
280 //         writefln("left %s right %s", left, right);
281 
282         // Linearly interpolate within the range
283         float t = nearestLine(leftPoint, rightPoint, point);
284         return left * (1 - t) + right * t;
285     }
286 
287     void splitAt(float off) {
288         vec2 pos = eval(off);
289         points.insertInPlace(1 + cast(uint)off, SplinePoint(pos, 1, 1));
290         interpolate();
291         if (target) target.splitAt(off);
292     }
293 
294     void prependPoint(vec2 point) {
295         points.insertInPlace(0, SplinePoint(point, 1, 1));
296         interpolate();
297         if (target) target.prependPoint(point);
298     }
299 
300     void appendPoint(vec2 point) {
301         points ~= SplinePoint(point, 1, 1);
302         interpolate();
303         if (target) target.appendPoint(point);
304     }
305 
306     int findPoint(vec2 point) {
307         uint bestIdx = 0;
308         float bestDist = float.infinity;
309         foreach(idx, pt; points) {
310             float dist = pt.position.distance(point);
311             if (dist < bestDist) {
312                 bestDist = dist;
313                 bestIdx = cast(uint)idx;
314             }
315         }
316 
317         if (bestDist > selectRadius/incViewportZoom) return -1;
318         return bestIdx;
319     }
320 
321     int addPoint(vec2 point) {
322         if (points.length < 2) {
323             appendPoint(point);
324             return cast(int)points.length - 1;
325         }
326 
327         float off = findClosestPointOffset(point);
328 //         writefln("Found off %s", off);
329         if (off <= 0) {
330             prependPoint(point);
331             return 0;
332         } else if (off >= (points.length - 1)) {
333             appendPoint(point);
334             return cast(int)points.length - 1;
335         }
336         vec2 onCurve = eval(off);
337         if (abs(point.distance(onCurve)) < selectRadius/incViewportZoom) {
338             splitAt(off);
339             return 1 + cast(int)off;
340         }
341         return -1;
342     }
343 
344     void removePoint(uint idx) {
345         points = points.remove(idx);
346         if (target) target.removePoint(idx);
347         interpolate();
348     }
349 
350     void draw(mat4 trans, vec4 color) {
351         if (drawLines.length > 0) {
352             inDbgSetBuffer(drawLines);
353             inDbgDrawLines(color, trans);
354         }
355         if (drawPoints.length > 0) {
356             inDbgSetBuffer(drawPoints);
357             inDbgPointsSize(10);
358             inDbgDrawPoints(vec4(0, 0, 0, 1), trans);
359             inDbgPointsSize(6);
360             inDbgDrawPoints(color, trans);
361         }
362     }
363 
364 }