// same as stroke_to() but with exception handling, should an // exception happen in the surface code (eg. out-of-memory) PyObject* python_stroke_to (Surface * surface, float x, float y, float pressure, float xtilt, float ytilt, double dtime) { bool res = stroke_to (surface, x, y, pressure, xtilt, ytilt, dtime); if (PyErr_Occurred()) { return NULL; } else if (res) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } }
// This function: // - is called once for each motion event // - does motion event interpolation // - paints zero, one or several dabs // - decides whether the stroke is finished (for undo/redo) // returns true if the stroke is finished or empty bool stroke_to (Surface * surface, float x, float y, float pressure, float xtilt, float ytilt, double dtime) { //printf("%f %f %f %f\n", (double)dtime, (double)x, (double)y, (double)pressure); float tilt_ascension = 0.0; float tilt_declination = 90.0; if (xtilt != 0 || ytilt != 0) { // shield us from insane tilt input xtilt = CLAMP(xtilt, -1.0, 1.0); ytilt = CLAMP(ytilt, -1.0, 1.0); assert(std::isfinite(xtilt) && std::isfinite(ytilt)); tilt_ascension = 180.0*atan2(-xtilt, ytilt)/M_PI; float e; if (abs(xtilt) > abs(ytilt)) { e = sqrt(1+ytilt*ytilt); } else { e = sqrt(1+xtilt*xtilt); } float rad = hypot(xtilt, ytilt); float cos_alpha = rad/e; if (cos_alpha >= 1.0) cos_alpha = 1.0; // fix numerical inaccuracy tilt_declination = 180.0*acos(cos_alpha)/M_PI; assert(std::isfinite(tilt_ascension)); assert(std::isfinite(tilt_declination)); } // printf("xtilt %f, ytilt %f\n", (double)xtilt, (double)ytilt); // printf("ascension %f, declination %f\n", (double)tilt_ascension, (double)tilt_declination); pressure = CLAMP(pressure, 0.0, 1.0); if (!std::isfinite(x) || !std::isfinite(y) || (x > 1e10 || y > 1e10 || x < -1e10 || y < -1e10)) { // workaround attempt for https://gna.org/bugs/?14372 g_print("Warning: ignoring brush::stroke_to with insane inputs (x = %f, y = %f)\n", (double)x, (double)y); x = 0.0; y = 0.0; pressure = 0.0; } // the assertion below is better than out-of-memory later at save time assert(x < 1e8 && y < 1e8 && x > -1e8 && y > -1e8); if (dtime < 0) g_print("Time jumped backwards by dtime=%f seconds!\n", dtime); if (dtime <= 0) dtime = 0.0001; // protect against possible division by zero bugs if (dtime > 0.100 && pressure && states[STATE_PRESSURE] == 0) { // Workaround for tablets that don't report motion events without pressure. // This is to avoid linear interpolation of the pressure between two events. stroke_to (surface, x, y, 0.0, 90.0, 0.0, dtime-0.0001); dtime = 0.0001; } g_rand_set_seed (rng, states[STATE_RNG_SEED]); { // calculate the actual "virtual" cursor position // noise first if (settings[BRUSH_TRACKING_NOISE]->base_value) { // OPTIMIZE: expf() called too often float base_radius = expf(settings[BRUSH_RADIUS_LOGARITHMIC]->base_value); x += rand_gauss (rng) * settings[BRUSH_TRACKING_NOISE]->base_value * base_radius; y += rand_gauss (rng) * settings[BRUSH_TRACKING_NOISE]->base_value * base_radius; } float fac = 1.0 - exp_decay (settings[BRUSH_SLOW_TRACKING]->base_value, 100.0*dtime); x = states[STATE_X] + (x - states[STATE_X]) * fac; y = states[STATE_Y] + (y - states[STATE_Y]) * fac; } // draw many (or zero) dabs to the next position // see doc/stroke2dabs.png float dist_moved = states[STATE_DIST]; float dist_todo = count_dabs_to (x, y, pressure, dtime); //if (dtime > 5 || dist_todo > 300) { if (dtime > 5 || reset_requested) { reset_requested = false; /* TODO: if (dist_todo > 300) { // this happens quite often, eg when moving the cursor back into the window // FIXME: bad to hardcode a distance treshold here - might look at zoomed image // better detect leaving/entering the window and reset then. g_print ("Warning: NOT drawing %f dabs.\n", dist_todo); g_print ("dtime=%f, dx=%f\n", dtime, x-states[STATE_X]); //must_reset = 1; } */ //printf("Brush reset.\n"); for (int i=0; i<STATE_COUNT; i++) { states[i] = 0; } states[STATE_X] = x; states[STATE_Y] = y; states[STATE_PRESSURE] = pressure; // not resetting, because they will get overwritten below: //dx, dy, dpress, dtime states[STATE_ACTUAL_X] = states[STATE_X]; states[STATE_ACTUAL_Y] = states[STATE_Y]; states[STATE_STROKE] = 1.0; // start in a state as if the stroke was long finished return true; } //g_print("dist = %f\n", states[STATE_DIST]); enum { UNKNOWN, YES, NO } painted = UNKNOWN; double dtime_left = dtime; float step_dx, step_dy, step_dpressure, step_dtime; float step_declination, step_ascension; while (dist_moved + dist_todo >= 1.0) { // there are dabs pending { // linear interpolation (nonlinear variant was too slow, see SVN log) float frac; // fraction of the remaining distance to move if (dist_moved > 0) { // "move" the brush exactly to the first dab (moving less than one dab) frac = (1.0 - dist_moved) / dist_todo; dist_moved = 0; } else { // "move" the brush from one dab to the next frac = 1.0 / dist_todo; } step_dx = frac * (x - states[STATE_X]); step_dy = frac * (y - states[STATE_Y]); step_dpressure = frac * (pressure - states[STATE_PRESSURE]); step_dtime = frac * (dtime_left - 0.0); step_declination = frac * (tilt_declination - states[STATE_DECLINATION]); step_ascension = frac * (tilt_ascension - states[STATE_ASCENSION]); // Though it looks different, time is interpolated exactly like x/y/pressure. } update_states_and_setting_values (step_dx, step_dy, step_dpressure, step_declination, step_ascension, step_dtime); bool painted_now = prepare_and_draw_dab (surface); if (painted_now) { painted = YES; } else if (painted == UNKNOWN) { painted = NO; } dtime_left -= step_dtime; dist_todo = count_dabs_to (x, y, pressure, dtime_left); } { // "move" the brush to the current time (no more dab will happen) // Important to do this at least once every event, because // brush_count_dabs_to depends on the radius and the radius can // depend on something that changes much faster than only every // dab (eg speed). step_dx = x - states[STATE_X]; step_dy = y - states[STATE_Y]; step_dpressure = pressure - states[STATE_PRESSURE]; step_declination = tilt_declination - states[STATE_DECLINATION]; step_ascension = tilt_ascension - states[STATE_ASCENSION]; step_dtime = dtime_left; //dtime_left = 0; but that value is not used any more update_states_and_setting_values (step_dx, step_dy, step_dpressure, step_declination, step_ascension, step_dtime); } // save the fraction of a dab that is already done now states[STATE_DIST] = dist_moved + dist_todo; //g_print("dist_final = %f\n", states[STATE_DIST]); // next seed for the RNG (GRand has no get_state() and states[] must always contain our full state) states[STATE_RNG_SEED] = g_rand_int(rng); // stroke separation logic (for undo/redo) if (painted == UNKNOWN) { if (stroke_current_idling_time > 0 || stroke_total_painting_time == 0) { // still idling painted = NO; } else { // probably still painting (we get more events than brushdabs) painted = YES; //if (pressure == 0) g_print ("info: assuming 'still painting' while there is no pressure\n"); } } if (painted == YES) { //if (stroke_current_idling_time > 0) g_print ("idling ==> painting\n"); stroke_total_painting_time += dtime; stroke_current_idling_time = 0; // force a stroke split after some time if (stroke_total_painting_time > 4 + 3*pressure) { // but only if pressure is not being released // FIXME: use some smoothed state for dpressure, not the output of the interpolation code // (which might easily wrongly give dpressure == 0) if (step_dpressure >= 0) { return true; } } } else if (painted == NO) { //if (stroke_current_idling_time == 0) g_print ("painting ==> idling\n"); stroke_current_idling_time += dtime; if (stroke_total_painting_time == 0) { // not yet painted, start a new stroke if we have accumulated a lot of irrelevant motion events if (stroke_current_idling_time > 1.0) { return true; } } else { // Usually we have pressure==0 here. But some brushes can paint // nothing at full pressure (eg gappy lines, or a stroke that // fades out). In either case this is the prefered moment to split. if (stroke_total_painting_time+stroke_current_idling_time > 0.9 + 5*pressure) { return true; } } } return false; }