nuclear@16: /* nuclear@16: curvedraw - a simple program to draw curves nuclear@16: Copyright (C) 2015 John Tsiombikas nuclear@16: nuclear@16: This program is free software: you can redistribute it and/or modify nuclear@16: it under the terms of the GNU General Public License as published by nuclear@16: the Free Software Foundation, either version 3 of the License, or nuclear@16: (at your option) any later version. nuclear@16: nuclear@16: This program is distributed in the hope that it will be useful, nuclear@16: but WITHOUT ANY WARRANTY; without even the implied warranty of nuclear@16: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the nuclear@16: GNU General Public License for more details. nuclear@16: nuclear@16: You should have received a copy of the GNU General Public License nuclear@16: along with this program. If not, see . nuclear@16: */ nuclear@0: #include nuclear@0: #include nuclear@2: #include nuclear@0: #include nuclear@0: #include nuclear@0: #include "opengl.h" nuclear@0: #include "app.h" nuclear@0: #include "curve.h" nuclear@0: #include "widgets.h" nuclear@6: #include "curvefile.h" nuclear@0: nuclear@3: enum SnapMode { nuclear@3: SNAP_NONE, nuclear@3: SNAP_GRID, nuclear@3: SNAP_POINT nuclear@3: }; nuclear@3: nuclear@1: int win_width, win_height; nuclear@1: float win_aspect; nuclear@1: nuclear@0: static void draw_grid(float sz, float sep, float alpha = 1.0f); nuclear@0: static void draw_curve(const Curve *curve); nuclear@0: static void on_click(int bn, float u, float v); nuclear@0: nuclear@2: // viewport control nuclear@2: static Vector2 view_pan; nuclear@3: static float view_scale = 0.2f; nuclear@3: static Matrix4x4 view_matrix; nuclear@3: nuclear@3: static float grid_size = 1.0; nuclear@3: static SnapMode snap_mode; nuclear@0: nuclear@12: static bool show_bounds; nuclear@12: nuclear@0: static std::vector curves; nuclear@2: static Curve *sel_curve; // selected curve being edited nuclear@2: static Curve *new_curve; // new curve being entered nuclear@2: static Curve *hover_curve; // curve the mouse is hovering over (click to select) nuclear@13: static int sel_pidx = -1; // selected point of the selected curve nuclear@13: static int hover_pidx = -1; // hovered over point nuclear@0: nuclear@2: static Label *weight_label; // floating label for the cp weight nuclear@0: nuclear@3: static Vector2 mouse_pointer; nuclear@3: nuclear@0: nuclear@0: bool app_init(int argc, char **argv) nuclear@0: { nuclear@5: glewInit(); nuclear@5: nuclear@0: glEnable(GL_MULTISAMPLE); nuclear@0: glEnable(GL_CULL_FACE); nuclear@2: nuclear@2: glEnable(GL_BLEND); nuclear@2: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); nuclear@0: return true; nuclear@0: } nuclear@0: nuclear@0: void app_cleanup() nuclear@0: { nuclear@0: for(size_t i=0; idraw(); nuclear@4: } nuclear@0: } nuclear@0: nuclear@0: static void draw_grid(float sz, float sep, float alpha) nuclear@0: { nuclear@0: float x = 0.0f; nuclear@3: float s = 1.0 / view_scale; nuclear@3: nuclear@3: sz *= s; nuclear@3: sz += sep; // one more step for when we have non-zero fractional pan nuclear@3: float end = std::min(sz, 100.0f * sep); nuclear@3: nuclear@3: // fractional pan nuclear@3: Vector2 pan = view_pan; nuclear@3: Vector2 fpan = Vector2(fmod(pan.x, sep), fmod(pan.y, sep)); nuclear@3: Vector2 offset = fpan - pan; nuclear@3: nuclear@3: glMatrixMode(GL_MODELVIEW); nuclear@3: glPushMatrix(); nuclear@3: glTranslatef(offset.x, offset.y, 0); nuclear@3: nuclear@3: glBegin(GL_LINES); nuclear@3: glColor4f(0.35, 0.35, 0.35, alpha); nuclear@3: while(x <= end) { nuclear@3: glVertex2f(-end, x); nuclear@3: glVertex2f(end, x); nuclear@3: glVertex2f(-end, -x); nuclear@3: glVertex2f(end, -x); nuclear@3: glVertex2f(x, -end); nuclear@3: glVertex2f(x, end); nuclear@3: glVertex2f(-x, -end); nuclear@3: glVertex2f(-x, end); nuclear@3: x += sep; nuclear@3: } nuclear@3: glEnd(); nuclear@3: glPopMatrix(); nuclear@3: nuclear@0: nuclear@0: glLineWidth(1.0); nuclear@0: glBegin(GL_LINES); nuclear@0: glColor4f(0.6, 0.3, 0.2, alpha); nuclear@3: glVertex2f(-sz + offset.x, 0); nuclear@3: glVertex2f(sz + offset.x, 0); nuclear@0: glColor4f(0.2, 0.3, 0.6, alpha); nuclear@3: glVertex2f(0, -sz + offset.y); nuclear@3: glVertex2f(0, sz + offset.y); nuclear@0: glEnd(); nuclear@3: nuclear@0: } nuclear@0: nuclear@0: static void draw_curve(const Curve *curve) nuclear@0: { nuclear@2: int numpt = curve->size(); nuclear@2: int segm = numpt * 16; nuclear@0: nuclear@12: if(show_bounds) { nuclear@12: Vector3 bmin, bmax; nuclear@12: curve->get_bbox(&bmin, &bmax); nuclear@12: nuclear@12: glLineWidth(1.0); nuclear@12: glColor3f(0, 1, 0); nuclear@12: glBegin(GL_LINE_LOOP); nuclear@12: glVertex2f(bmin.x, bmin.y); nuclear@12: glVertex2f(bmax.x, bmin.y); nuclear@12: glVertex2f(bmax.x, bmax.y); nuclear@12: glVertex2f(bmin.x, bmax.y); nuclear@12: glEnd(); nuclear@12: } nuclear@12: nuclear@2: glLineWidth(curve == hover_curve ? 4.0 : 2.0); nuclear@1: if(curve == sel_curve) { nuclear@1: glColor3f(0.3, 0.4, 1.0); nuclear@1: } else if(curve == new_curve) { nuclear@1: glColor3f(1.0, 0.75, 0.3); nuclear@1: } else { nuclear@2: glColor3f(0.6, 0.6, 0.6); nuclear@1: } nuclear@0: glBegin(GL_LINE_STRIP); nuclear@0: for(int i=0; iinterpolate(t); nuclear@0: glVertex2f(v.x, v.y); nuclear@0: } nuclear@0: glEnd(); nuclear@0: glLineWidth(1.0); nuclear@0: nuclear@2: glPointSize(curve == hover_curve ? 10.0 : 7.0); nuclear@0: glBegin(GL_POINTS); nuclear@1: if(curve == new_curve) { nuclear@1: glColor3f(1.0, 0.0, 0.0); nuclear@1: } else { nuclear@1: glColor3f(0.6, 0.3, 0.2); nuclear@1: } nuclear@0: for(int i=0; iget_point2(i); nuclear@0: glVertex2f(pt.x, pt.y); nuclear@0: } nuclear@0: glEnd(); nuclear@12: nuclear@12: // draw the projected mouse point on the selected curve nuclear@12: /* nuclear@12: if(curve == sel_curve) { nuclear@12: Vector3 pp = curve->proj_point(Vector3(mouse_pointer.x, mouse_pointer.y, 0.0)); nuclear@12: nuclear@12: glPointSize(5.0); nuclear@12: glBegin(GL_POINTS); nuclear@12: glColor3f(1, 0.8, 0.2); nuclear@12: glVertex2f(pp.x, pp.y); nuclear@12: glEnd(); nuclear@12: } nuclear@12: */ nuclear@0: glPointSize(1.0); nuclear@0: } nuclear@0: nuclear@0: void app_reshape(int x, int y) nuclear@0: { nuclear@0: win_width = x; nuclear@0: win_height = y; nuclear@0: win_aspect = (float)x / (float)y; nuclear@0: nuclear@0: glViewport(0, 0, x, y); nuclear@0: glMatrixMode(GL_PROJECTION); nuclear@0: glLoadIdentity(); nuclear@0: glOrtho(-win_aspect, win_aspect, -1, 1, -1, 1); nuclear@0: } nuclear@0: nuclear@0: void app_keyboard(int key, bool pressed) nuclear@0: { nuclear@0: if(pressed) { nuclear@0: switch(key) { nuclear@1: case 'q': nuclear@1: case 'Q': nuclear@1: exit(0); nuclear@1: nuclear@0: case 27: nuclear@1: if(new_curve) { nuclear@1: delete new_curve; nuclear@1: new_curve = 0; nuclear@1: post_redisplay(); nuclear@1: } nuclear@1: break; nuclear@0: nuclear@12: case '1': nuclear@12: case '2': nuclear@12: case '3': nuclear@0: if(sel_curve) { nuclear@12: sel_curve->set_type((CurveType)((int)CURVE_LINEAR + key - '1')); nuclear@0: post_redisplay(); nuclear@0: } nuclear@1: if(new_curve) { nuclear@12: new_curve->set_type((CurveType)((int)CURVE_LINEAR + key - '1')); nuclear@1: post_redisplay(); nuclear@1: } nuclear@0: break; nuclear@0: nuclear@0: case 'b': nuclear@0: case 'B': nuclear@12: show_bounds = !show_bounds; nuclear@12: post_redisplay(); nuclear@0: break; nuclear@0: nuclear@12: case 'n': nuclear@12: case 'N': nuclear@0: if(sel_curve) { nuclear@12: sel_curve->normalize(); nuclear@1: post_redisplay(); nuclear@1: } nuclear@0: break; nuclear@6: nuclear@6: case 'e': nuclear@6: case 'E': nuclear@6: // TODO: GUI for filename at least nuclear@10: if(!save_curves("test.curves", &curves[0], (int)curves.size())) { nuclear@6: fprintf(stderr, "failed to export curves\n"); nuclear@6: } nuclear@6: printf("exported %d curves\n", (int)curves.size()); nuclear@6: break; nuclear@10: nuclear@12: case 'l': nuclear@12: case 'L': nuclear@10: { nuclear@10: std::list clist = load_curves("test.curves"); nuclear@10: if(clist.empty()) { nuclear@10: fprintf(stderr, "failed to import curves\n"); nuclear@10: } nuclear@10: nuclear@12: for(size_t i=0; i::iterator it = clist.begin(); nuclear@10: while(it != clist.end()) { nuclear@10: curves.push_back(*it++); nuclear@10: ++num; nuclear@10: } nuclear@10: printf("imported %d curves\n", num); nuclear@10: } nuclear@12: post_redisplay(); nuclear@10: break; nuclear@0: } nuclear@0: } nuclear@3: nuclear@3: nuclear@3: switch(key) { nuclear@3: case 's': nuclear@3: snap_mode = pressed ? SNAP_GRID : SNAP_NONE; nuclear@3: break; nuclear@3: nuclear@3: case 'S': nuclear@3: snap_mode = pressed ? SNAP_POINT : SNAP_NONE; nuclear@3: break; nuclear@3: nuclear@3: default: nuclear@3: break; nuclear@3: } nuclear@3: } nuclear@3: nuclear@3: static void calc_view_matrix() nuclear@3: { nuclear@3: view_matrix.reset_identity(); nuclear@3: view_matrix.scale(Vector3(view_scale, view_scale, view_scale)); nuclear@3: view_matrix.translate(Vector3(view_pan.x, view_pan.y, 0.0)); nuclear@0: } nuclear@0: nuclear@0: static Vector2 pixel_to_uv(int x, int y) nuclear@0: { nuclear@0: float u = win_aspect * (2.0 * (float)x / (float)win_width - 1.0); nuclear@0: float v = 1.0 - 2.0 * (float)y / (float)win_height; nuclear@3: nuclear@3: u = u / view_scale - view_pan.x; nuclear@3: v = v / view_scale - view_pan.y; nuclear@0: return Vector2(u, v); nuclear@3: /* nuclear@3: Matrix4x4 inv_view_matrix = view_matrix.inverse(); nuclear@3: Vector4 res = Vector4(u, v, 0.0, 1.0).transformed(inv_view_matrix); nuclear@3: nuclear@3: return Vector2(res.x, res.y); nuclear@3: */ nuclear@0: } nuclear@0: nuclear@0: static int prev_x, prev_y; nuclear@0: static int click_pos[8][2]; nuclear@0: static unsigned int bnstate; nuclear@0: nuclear@0: #define BNBIT(x) (1 << (x)) nuclear@0: nuclear@0: void app_mouse_button(int bn, bool pressed, int x, int y) nuclear@0: { nuclear@0: prev_x = x; nuclear@0: prev_y = y; nuclear@0: if(pressed) { nuclear@0: bnstate |= BNBIT(bn); nuclear@0: } else { nuclear@0: bnstate &= ~BNBIT(bn); nuclear@0: } nuclear@0: nuclear@0: if(pressed) { nuclear@0: click_pos[bn][0] = x; nuclear@0: click_pos[bn][1] = y; nuclear@0: } else { nuclear@0: int dx = x - click_pos[bn][0]; nuclear@0: int dy = y - click_pos[bn][1]; nuclear@0: nuclear@0: if(abs(dx) + abs(dy) < 3) { nuclear@0: Vector2 uv = pixel_to_uv(x, y); nuclear@0: on_click(bn, uv.x, uv.y); nuclear@0: } nuclear@0: nuclear@0: if(!(bnstate & BNBIT(2))) { nuclear@0: delete weight_label; nuclear@0: weight_label = 0; nuclear@0: post_redisplay(); nuclear@0: } nuclear@0: } nuclear@0: } nuclear@0: nuclear@2: static bool point_hit_test(const Vector2 &pos, Curve **curveret, int *pidxret) nuclear@0: { nuclear@3: float thres = 0.02 / view_scale; nuclear@0: nuclear@0: for(size_t i=0; inearest_point(pos); nuclear@0: if(pidx == -1) continue; nuclear@0: nuclear@13: Vector2 cp = curves[i]->get_point2(pidx); nuclear@2: if((cp - pos).length_sq() < thres * thres) { nuclear@2: *curveret = curves[i]; nuclear@2: *pidxret = pidx; nuclear@2: return true; nuclear@0: } nuclear@0: } nuclear@2: *curveret = 0; nuclear@2: *pidxret = -1; nuclear@2: return false; nuclear@0: } nuclear@0: nuclear@13: static bool hit_test(const Vector2 &pos, Curve **curveret, int *pidxret) nuclear@13: { nuclear@13: float thres = 0.02 / view_scale; nuclear@13: nuclear@13: if(point_hit_test(pos, curveret, pidxret)) { nuclear@13: return true; nuclear@13: } nuclear@13: nuclear@13: Vector3 pos3 = Vector3(pos.x, pos.y, 0.0f); nuclear@13: for(size_t i=0; idistance_sq(pos3)) < thres * thres) { nuclear@13: *curveret = curves[i]; nuclear@13: *pidxret = -1; nuclear@13: return true; nuclear@13: } nuclear@13: } nuclear@13: *curveret = 0; nuclear@13: *pidxret = -1; nuclear@13: return false; nuclear@13: } nuclear@13: nuclear@3: static Vector2 snap(const Vector2 &p) nuclear@3: { nuclear@3: switch(snap_mode) { nuclear@3: case SNAP_GRID: nuclear@3: return Vector2(round(p.x / grid_size) * grid_size, round(p.y / grid_size) * grid_size); nuclear@3: case SNAP_POINT: nuclear@14: { nuclear@14: Curve *nearest_curve = 0; nuclear@14: int nearest_curve_pidx = -1; nuclear@14: float nearest_dist_sq = FLT_MAX; nuclear@14: nuclear@14: if(new_curve) { nuclear@14: // find the closest point, ignoring the last nuclear@14: for(int i=0; isize() - 1; i++) { nuclear@14: Vector2 cp = new_curve->get_point(i); nuclear@14: float distsq = (cp - p).length_sq(); nuclear@14: if(distsq < nearest_dist_sq) { nuclear@14: nearest_curve = new_curve; nuclear@14: nearest_dist_sq = distsq; nuclear@14: nearest_curve_pidx = i; nuclear@14: } nuclear@14: } nuclear@14: } nuclear@14: nuclear@14: nuclear@14: for(size_t i=0; inearest_point(p); nuclear@14: Vector2 cp = curves[i]->get_point(pidx); nuclear@14: float dist_sq = (cp - p).length_sq(); nuclear@14: if(dist_sq < nearest_dist_sq) { nuclear@14: nearest_curve = curves[i]; nuclear@14: nearest_curve_pidx = pidx; nuclear@14: nearest_dist_sq = dist_sq; nuclear@14: } nuclear@14: } nuclear@14: nuclear@14: if(nearest_curve) { nuclear@14: return nearest_curve->get_point(nearest_curve_pidx); nuclear@14: } nuclear@14: } nuclear@14: break; nuclear@14: nuclear@3: default: nuclear@3: break; nuclear@3: } nuclear@3: return p; nuclear@3: } nuclear@3: nuclear@0: void app_mouse_motion(int x, int y) nuclear@0: { nuclear@3: Vector2 prev_uv = pixel_to_uv(prev_x, prev_y); nuclear@3: nuclear@0: int dx = x - prev_x; nuclear@0: int dy = y - prev_y; nuclear@0: prev_x = x; nuclear@0: prev_y = y; nuclear@0: nuclear@0: if(!dx && !dy) return; nuclear@0: nuclear@2: Vector2 uv = pixel_to_uv(x, y); nuclear@3: mouse_pointer = uv; nuclear@13: //post_redisplay(); nuclear@2: nuclear@2: /* when entering a new curve, have the last (extra) point following nuclear@2: * the mouse until it's entered by a click (see on_click). nuclear@2: */ nuclear@3: if(new_curve) { nuclear@3: new_curve->move_point(new_curve->size() - 1, snap(uv)); nuclear@2: post_redisplay(); nuclear@2: } nuclear@2: nuclear@4: if(!new_curve && !bnstate) { nuclear@3: // not dragging, highlight curve under mouse nuclear@13: hit_test(uv, &hover_curve, &hover_pidx); nuclear@13: if(hover_curve == sel_curve) { nuclear@13: sel_pidx = hover_pidx; nuclear@13: } nuclear@0: post_redisplay(); nuclear@0: nuclear@3: } else { nuclear@3: // we're dragging with one or more buttons held down nuclear@0: nuclear@3: if(sel_curve && sel_pidx != -1) { nuclear@3: // we have a curve and a point of the curve selected nuclear@0: nuclear@3: if(bnstate & BNBIT(0)) { nuclear@3: // dragging point with left button: move it nuclear@3: sel_curve->move_point(sel_pidx, snap(uv)); nuclear@3: post_redisplay(); nuclear@0: } nuclear@3: nuclear@3: if(bnstate & BNBIT(2)) { nuclear@3: // dragging point with right button: change weight nuclear@3: float w = sel_curve->get_weight(sel_pidx); nuclear@3: w -= dy * 0.01; nuclear@3: if(w < FLT_MIN) w = FLT_MIN; nuclear@3: sel_curve->set_weight(sel_pidx, w); nuclear@3: nuclear@3: // popup floating weight label if not already there nuclear@3: if(!weight_label) { nuclear@3: weight_label = new Label; nuclear@3: } nuclear@3: weight_label->set_position(uv); nuclear@3: weight_label->set_textf("w=%g", w); nuclear@3: post_redisplay(); nuclear@3: } nuclear@3: } else { nuclear@3: // no selection, we're dragging in empty space: manipulate viewport nuclear@3: Vector2 dir = uv - prev_uv; nuclear@3: nuclear@3: if(bnstate & (BNBIT(0) | BNBIT(1))) { nuclear@3: // panning nuclear@3: view_pan += dir; nuclear@3: calc_view_matrix(); nuclear@3: post_redisplay(); nuclear@3: } nuclear@3: if(bnstate & BNBIT(2)) { nuclear@3: // zooming nuclear@3: view_scale -= ((float)dy / (float)win_height) * view_scale * 5.0; nuclear@3: if(view_scale < 1e-4) view_scale = 1e-4; nuclear@3: calc_view_matrix(); nuclear@3: post_redisplay(); nuclear@3: } nuclear@0: } nuclear@0: } nuclear@0: } nuclear@0: nuclear@0: static void on_click(int bn, float u, float v) nuclear@0: { nuclear@2: Vector2 uv = Vector2(u, v); nuclear@2: nuclear@0: switch(bn) { nuclear@2: case 0: // ------- LEFT CLICK ------ nuclear@2: if(hover_curve) { nuclear@2: // if we're hovering: click selects nuclear@2: sel_curve = hover_curve; nuclear@13: sel_pidx = hover_pidx; nuclear@2: hover_curve = 0; nuclear@2: } else if(sel_curve) { nuclear@2: // if we have a selected curve: click adds point (enter new_curve mode) nuclear@2: std::vector::iterator it = std::find(curves.begin(), curves.end(), sel_curve); nuclear@2: assert(it != curves.end()); nuclear@2: curves.erase(it, it + 1); nuclear@2: nuclear@2: new_curve = sel_curve; nuclear@2: sel_curve = 0; nuclear@2: sel_pidx = -1; nuclear@2: nuclear@2: new_curve->add_point(uv); nuclear@2: } else { nuclear@2: // otherwise, click starts a new curve nuclear@2: if(!new_curve) { nuclear@2: new_curve = new Curve; nuclear@2: new_curve->add_point(uv); nuclear@2: } nuclear@2: new_curve->add_point(uv); nuclear@0: } nuclear@0: post_redisplay(); nuclear@0: break; nuclear@0: nuclear@2: case 2: // ------- RIGHT CLICK ------ nuclear@2: if(new_curve) { nuclear@2: // in new-curve mode: finish curve (cancels last floating segment) nuclear@2: new_curve->remove_point(new_curve->size() - 1); nuclear@2: if(new_curve->empty()) { nuclear@2: delete new_curve; nuclear@2: } else { nuclear@2: curves.push_back(new_curve); nuclear@2: } nuclear@2: new_curve = 0; nuclear@2: nuclear@2: } else if(sel_curve) { nuclear@2: // in selected curve mode: delete control point or unselect nuclear@2: Curve *hit_curve; nuclear@2: int hit_pidx; nuclear@13: if(hit_test(uv, &hit_curve, &hit_pidx) && hit_curve == sel_curve) { nuclear@13: if(hit_pidx != -1) { nuclear@13: hit_curve->remove_point(hit_pidx); nuclear@13: sel_pidx = -1; nuclear@13: } nuclear@2: } else { nuclear@2: sel_curve = 0; nuclear@2: sel_pidx = -1; nuclear@2: } nuclear@2: } nuclear@0: post_redisplay(); nuclear@0: break; nuclear@0: nuclear@0: default: nuclear@0: break; nuclear@0: } nuclear@0: }