From 9980fd0b8e1f3a07060316f28469f55a3f2fc0cd Mon Sep 17 00:00:00 2001 From: Joseph Eagar Date: Sat, 5 Nov 2022 14:47:48 -0700 Subject: temp-sculpt-roll-mapping: Port roll tex mapping code from sculpt-dev --- source/blender/blenlib/BLI_even_spline.hh | 733 +++++++ source/blender/editors/sculpt_paint/CMakeLists.txt | 2 +- source/blender/editors/sculpt_paint/paint_intern.h | 16 + source/blender/editors/sculpt_paint/paint_stroke.c | 1700 ---------------- .../blender/editors/sculpt_paint/paint_stroke.cc | 2065 ++++++++++++++++++++ source/blender/editors/sculpt_paint/sculpt.c | 31 + .../blender/editors/sculpt_paint/sculpt_intern.h | 2 + source/blender/makesdna/DNA_texture_types.h | 1 + source/blender/makesrna/intern/rna_brush.c | 1 + 9 files changed, 2850 insertions(+), 1701 deletions(-) create mode 100644 source/blender/blenlib/BLI_even_spline.hh delete mode 100644 source/blender/editors/sculpt_paint/paint_stroke.c create mode 100644 source/blender/editors/sculpt_paint/paint_stroke.cc diff --git a/source/blender/blenlib/BLI_even_spline.hh b/source/blender/blenlib/BLI_even_spline.hh new file mode 100644 index 00000000000..4b80df0202c --- /dev/null +++ b/source/blender/blenlib/BLI_even_spline.hh @@ -0,0 +1,733 @@ +#pragma once + +#include "BLI_compiler_attrs.h" +#include "BLI_compiler_compat.h" + +#include "BLI_math.h" +#include "BLI_math_vec_types.hh" +#include "BLI_vector.hh" + +#include +#include + +//#define FINITE_DIFF + +/* + * Arc length parameterized spline library. + */ +namespace blender { +/* + Abstract curve interface. + +template class Curve { + using Vector = vec_base; + + public: + Float length; + + Vector evaluate(Float s); + Vector derivative(Float s); + Vector derivative2(Float s); + Float curvature(float s); + + void update(); +}; +*/ + +/* +comment: Reduce algebra script; + +on factor; +off period; + +procedure bez(a, b); + a + (b - a) * t; + +lin := bez(k1, k2); +quad := bez(lin, sub(k2=k3, k1=k2, lin)); + +cubic := bez(quad, sub(k3=k4, k2=k3, k1=k2, quad)); +dcubic := df(cubic, t); +icubic := int(cubic, t); + +x1 := 0; +y1 := 0; + +dx := sub(k1=x1, k2=x2, k3=x3, k4=x4, dcubic); +dy := sub(k1=y1, k2=y2, k3=y3, k4=y4, dcubic); +darc := sqrt(dx**2 + dy**2); + +arcstep := darc*dt + 0.5*df(darc, t)*dt*dt; + +d2x := df(dx / darc, t); +d2y := df(dy / darc, t); + +gentran +begin +declare << +x1,x2,x3,x4 : float; +y1,y2,y3,y4 : float; +dt,t : float; +>>; +return eval(arcstep) +end; + +on fort; +cubic; +dcubic; +icubic; +arcstep; +d2x; +d2y; +off fort; + +*/ +template class CubicBezier { + using Vector = vec_base; + + public: + Vector ps[4]; + + CubicBezier(Vector a, Vector b, Vector c, Vector d) + { + ps[0] = a; + ps[1] = b; + ps[2] = c; + ps[3] = d; + + deleted = false; + _arc_to_t = new Float[table_size]; + } + + ~CubicBezier() + { + deleted = true; + + if (_arc_to_t) { + delete[] _arc_to_t; + _arc_to_t = nullptr; + } + } + + CubicBezier() + { + deleted = false; + _arc_to_t = new Float[table_size]; + } + + CubicBezier(const CubicBezier &b) + { + _arc_to_t = new Float[table_size]; + *this = b; + deleted = false; + } + + CubicBezier &operator=(const CubicBezier &b) + { + ps[0] = b.ps[0]; + ps[1] = b.ps[1]; + ps[2] = b.ps[2]; + ps[3] = b.ps[3]; + + length = b.length; + + if (!_arc_to_t) { + _arc_to_t = new Float[table_size]; + } + + if (b._arc_to_t) { + for (int i = 0; i < table_size; i++) { + _arc_to_t[i] = b._arc_to_t[i]; + } + } + + return *this; + } + +#if 1 + CubicBezier(CubicBezier &&b) + { + *this = b; + } + + CubicBezier &operator=(CubicBezier &&b) + { + ps[0] = b.ps[0]; + ps[1] = b.ps[1]; + ps[2] = b.ps[2]; + ps[3] = b.ps[3]; + + length = b.length; + + if (b._arc_to_t) { + _arc_to_t = std::move(b._arc_to_t); + b._arc_to_t = nullptr; + } + else { + _arc_to_t = new Float[table_size]; + } + + return *this; + } +#endif + + Float length; + + void update() + { + Float t = 0.0, dt = 1.0 / (Float)table_size; + Float s = 0.0; + + if (!_arc_to_t) { + _arc_to_t = new Float[table_size]; + } + + auto table = _arc_to_t; + + for (int i = 0; i < table_size; i++) { + table[i] = -1.0; + } + + length = 0.0; + + for (int i = 0; i < table_size; i++, t += dt) { + Float dlen = 0.0; + for (int j = 0; j < axes; j++) { + float dv = dcubic(ps[0][j], ps[1][j], ps[2][j], ps[3][j], t); + + dlen += dv * dv; + } + + dlen = sqrt(dlen) * dt; + + length += dlen; + } + + const int samples = table_size; + dt = 1.0 / (Float)samples; + + t = 0.0; + s = 0.0; + + for (int i = 0; i < samples; i++, t += dt) { + Float dlen = 0.0; + for (int j = 0; j < axes; j++) { + float dv = dcubic(ps[0][j], ps[1][j], ps[2][j], ps[3][j], t); + + dlen += dv * dv; + } + + dlen = sqrt(dlen) * dt; + + int j = (int)((s / length) * (Float)table_size * 0.999999); + j = min_ii(j, table_size - 1); + + table[j] = t; + + s += dlen; + } + + table[0] = 0.0; + table[table_size - 1] = 1.0; + +#if 1 + /* Interpolate gaps in table. */ + for (int i = 0; i < table_size - 1; i++) { + if (table[i] == -1.0 || table[i + 1] != -1.0) { + continue; + } + + int i1 = i; + int i2 = i + 1; + + while (table[i2] == -1.0) { + i2++; + } + + int start = table[i1]; + int end = table[i2]; + Float dt2 = 1.0 / (i2 - i1); + + for (int j = i1 + 1; j < i2; j++) { + Float factor = (Float)(j - i1) * dt2; + table[j] = start + (end - start) * factor; + } + + i = i2 - 1; + } + +# if 0 + for (int i = 0; i < table_size; i++) { + printf("%.3f ", table[i]); + } + printf("\n\n"); +# endif +#endif + } + + inline Vector evaluate(Float s) + { + Float t = arc_to_t(s); + Vector r; + + for (int i = 0; i < axes; i++) { + r[i] = cubic(ps[0][i], ps[1][i], ps[2][i], ps[3][i], t); + } + + return r; + } + + Vector derivative(Float s, bool exact = true) + { + Float t = arc_to_t(s); + Vector r; + + for (int i = 0; i < axes; i++) { + r[i] = dcubic(ps[0][i], ps[1][i], ps[2][i], ps[3][i], t) * length; + } + + /* Real arc length parameterized tangent has unit length. */ + if (exact) { + Float len = sqrt(_dot(r, r)); + + if (len > 0.00001) { + r = r / len; + } + } + + return r; + } + + Vector derivative2(Float s) + { +#ifdef FINITE_DIFF + const Float df = 0.0005; + Float s1, s2; + + if (s >= 1.0 - df) { + s1 = s - df; + s2 = s; + } + else { + s1 = s; + s2 = s + df; + } + + Vector a = derivative(s1); + Vector b = derivative(s2); + + return (b - a) / df; +#else + Float t = arc_to_t(s); + Vector r; + + Float dx = dcubic(ps[0][0], ps[1][0], ps[2][0], ps[3][0], t); + Float d2x = d2cubic(ps[0][0], ps[1][0], ps[2][0], ps[3][0], t); + Float dy = dcubic(ps[0][1], ps[1][1], ps[2][1], ps[3][1], t); + Float d2y = d2cubic(ps[0][1], ps[1][1], ps[2][1], ps[3][1], t); + + /* + comment: arc length second derivative; + + operator x, y, z, dx, dy, dz, d2x, d2y, d2z; + forall t let df(x(t), t) = dx(t); + forall t let df(y(t), t) = dy(t); + forall t let df(z(t), t) = dz(t); + forall t let df(dx(t), t) = d2x(t); + forall t let df(dy(t), t) = d2y(t); + forall t let df(dz(t), t) = d2z(t); + + comment: arc length first derivative is just the normalized tangent; + + comment: 2d case; + + dlen := sqrt(df(x(t), t)**2 + df(y(t), t)**2); + + df(df(x(t), t) / dlen, t); + df(df(y(t), t) / dlen, t); + + comment: 3d case; + + dlen := sqrt(df(x(t), t)**2 + df(y(t), t)**2 + df(z(t), t)**2); + + comment: final derivatives; + + df(df(x(t), t) / dlen, t); + df(df(y(t), t) / dlen, t); + df(df(z(t), t) / dlen, t); + */ + if constexpr (axes == 2) { + /* Basically the 2d perpidicular normalized tangent multiplied by the curvature. */ + + Float div = sqrt(dx * dx + dy * dy) * (dx * dx + dy * dy); + + r[0] = ((d2x * dy - d2y * dx) * dy) / div; + r[1] = (-(d2x * dy - d2y * dx) * dx) / div; + } + else if constexpr (axes == 3) { + Float dz = dcubic(ps[0][2], ps[1][2], ps[2][2], ps[3][2], t); + Float d2z = d2cubic(ps[0][2], ps[1][2], ps[2][2], ps[3][2], t); + + Float div = sqrt(dx * dx + dy * dy + dz * dz) * (dy * dy + dz * dz + dx * dx); + + r[0] = (d2x * dy * dy + d2x * dz * dz - d2y * dx * dy - d2z * dx * dz) / div; + r[1] = (-(d2x * dx * dy - d2y * dx * dx - d2y * dz * dz + d2z * dy * dz)) / div; + r[2] = (-(d2x * dx * dz + d2y * dy * dz - d2z * dx * dx - d2z * dy * dy)) / div; + } + else { + for (int i = 0; i < axes; i++) { + r[i] = d2cubic(ps[0][i], ps[1][i], ps[2][i], ps[3][i], t) * length; + } + } + + return r; +#endif + } + + Float curvature(Float s) + { + Vector dv2 = derivative2(s); + + if constexpr (axes == 2) { + Vector dv = derivative(s, true); + + /* Calculate signed curvature. Remember that dv is normalized. */ + return dv[0] * dv2[1] - dv[1] * dv2[0]; + } + + return sqrt(_dot(dv2, dv2)); + } + + private: + Float *_arc_to_t; + bool deleted = false; + + Float cubic(Float k1, Float k2, Float k3, Float k4, Float t) + { + return -(((3.0 * (t - 1.0) * k3 - k4 * t) * t - 3.0 * (t - 1.0) * (t - 1.0) * k2) * t + + (t - 1) * (t - 1) * (t - 1) * k1); + } + + Float dcubic(Float k1, Float k2, Float k3, Float k4, Float t) + { + return -3.0 * ((t - 1.0) * (t - 1.0) * k1 - k4 * t * t + (3.0 * t - 2.0) * k3 * t - + (3.0 * t - 1.0) * (t - 1.0) * k2); + } + + Float d2cubic(Float k1, Float k2, Float k3, Float k4, Float t) + { + return -6.0 * (k1 * t - k1 - 3.0 * k2 * t + 2.0 * k2 + 3.0 * k3 * t - k3 - k4 * t); + } + + Float _dot(Vector a, Vector b) + { + Float sum = 0.0; + + for (int i = 0; i < axes; i++) { + sum += a[i] * b[i]; + } + + return sum; + } + + Float clamp_s(Float s) + { + s = s < 0.0 ? 0.0 : s; + s = s >= length ? length * 0.999999 : s; + + return s; + } + + Float arc_to_t(Float s) + { + if (length == 0.0) { + return 0.0; + } + + s = clamp_s(s); + + Float t = s * (Float)(table_size - 1) / length; + + int i1 = floorf(t); + int i2 = min_ii(i1 + 1, table_size - 1); + + t -= (Float)i1; + + Float s1 = _arc_to_t[i1]; + Float s2 = _arc_to_t[i2]; + + return s1 + (s2 - s1) * t; + } +}; + +template class BezierSpline { + using Vector = vec_base; + struct Segment { + CubicBezier bezier; + Float start = 0.0; + + Segment(const CubicBezier &bez) + { + bezier = bez; + } + + Segment(const Segment &b) + { + *this = b; + } + + Segment &operator=(const Segment &b) + { + bezier = b.bezier; + start = b.start; + + return *this; + } + + Segment() + { + } + }; + + public: + Float length = 0.0; + bool deleted = false; + blender::Vector segments; + + void clear() + { + segments.clear(); + } + + BezierSpline() + { + } + + ~BezierSpline() + { + deleted = true; + } + + void add(CubicBezier &bez) + { + need_update = true; + + Segment seg; + + seg.bezier = bez; + segments.append(seg); + + update(); + } + + void update() + { + need_update = false; + + length = 0.0; + for (Segment &seg : segments) { + seg.start = length; + length += seg.bezier.length; + } + } + + inline Vector evaluate(Float s) + { + if (s == 0.0) { + return segments[0].bezier.ps[0]; + } + + if (s >= length) { + return segments[segments.size() - 1].bezier.ps[3]; + } + + Segment *seg = get_segment(s); + + return seg->bezier.evaluate(s - seg->start); + } + + Vector derivative(Float s, bool exact = true) + { + if (segments.size() == 0) { + return Vector(); + } + + s = clamp_s(s); + Segment *seg = get_segment(s); + + return seg->bezier.derivative(s - seg->start, exact); + } + + Vector derivative2(Float s) + { + if (segments.size() == 0) { + return Vector(); + } + + s = clamp_s(s); + Segment *seg = get_segment(s); + + return seg->bezier.derivative2(s - seg->start); + } + + Float curvature(Float s) + { + if (segments.size() == 0) { + return 0.0; + } + + s = clamp_s(s); + Segment *seg = get_segment(s); + + return seg->bezier.curvature(s - seg->start); + } + + /* Find the closest point on the spline. Uses a bisecting root finding approach. + * Note: in thoery we could split the spline into quadratic segments and solve + * for the closest point directy. + */ + Vector closest_point(const Vector p, Float &r_s, Vector &r_tan, Float &r_dis) + { + if (segments.size() == 0) { + return Vector(); + } + + const int steps = 12; + Float s = 0.0, ds = length / steps; + Float mindis = FLT_MAX; + Vector minp; + Float mins = 0.0; + bool found = false; + + Vector lastdv, lastp; + Vector b, dvb; + + for (int i = 0; i < steps + 1; i++, s += ds, lastp = b, lastdv = dvb) { + b = evaluate(s); + dvb = derivative(s, false); /* We don't need real normalized derivative here. */ + + if (i == 0) { + continue; + } + + Vector dva = lastdv; + Vector a = lastp; + + Vector vec1 = a - p; + Vector vec2 = b - p; + + Float sign1 = _dot(vec1, dva); + Float sign2 = _dot(vec2, dvb); + + if ((sign1 < 0.0) == (sign2 < 0.0)) { + found = true; + + Float len = _dot(vec1, vec1); + + if (len < mindis) { + mindis = len; + mins = s; + minp = evaluate(s); + } + continue; + } + + found = true; + + Float start = s - ds; + Float end = s; + Float mid = (start + end) * 0.5; + const int binary_steps = 10; + + for (int j = 0; j < binary_steps; j++) { + Vector dvmid = derivative(mid, false); + Vector vecmid = evaluate(mid) - p; + Float sign_mid = _dot(vecmid, dvmid); + + if ((sign_mid < 0.0) == (sign1 < 0.0)) { + start = mid; + } + else { + end = mid; + } + mid = (start + end) * 0.5; + } + + Vector p2 = evaluate(mid); + Vector vec_mid = p2 - p; + Float len = _dot(vec_mid, vec_mid); + + if (len < mindis) { + mindis = len; + minp = p2; + mins = mid; + } + } + + if (!found) { + mins = 0.0; + minp = evaluate(mins); + Vector vec = minp - p; + mindis = _dot(vec, vec); + } + + r_tan = derivative(mins, true); + r_s = mins; + r_dis = sqrtf(mindis); + + return minp; + } + + void pop_front(int n = 1) + { + for (int i = 0; i < segments.size() - n; i++) { + segments[i] = segments[i + n]; + } + + segments.resize(segments.size() - n); + update(); + } + + private: + bool need_update; + + Float _dot(Vector a, Vector b) + { + Float sum = 0.0; + + for (int i = 0; i < axes; i++) { + sum += a[i] * b[i]; + } + + return sum; + } + + Float clamp_s(Float s) + { + s = s < 0.0 ? 0.0 : s; + s = s >= length ? length * 0.999999 : s; + + return s; + } + + Segment *get_segment(Float s) + { + // printf("\n"); + + for (Segment &seg : segments) { + // printf("s: %f %f\n", seg.start, seg.start + seg.bezier.length); + + if (s >= seg.start && s < seg.start + seg.bezier.length) { + return &seg; + } + } + + // printf("\n"); + + return nullptr; + } +}; + +using BezierSpline2f = BezierSpline; +using BezierSpline3f = BezierSpline; +} // namespace blender diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt index 2709ac3fd91..b43713462b0 100644 --- a/source/blender/editors/sculpt_paint/CMakeLists.txt +++ b/source/blender/editors/sculpt_paint/CMakeLists.txt @@ -53,7 +53,7 @@ set(SRC paint_image_proj.c paint_mask.c paint_ops.c - paint_stroke.c + paint_stroke.cc paint_utils.c paint_vertex.cc paint_vertex_color_ops.cc diff --git a/source/blender/editors/sculpt_paint/paint_intern.h b/source/blender/editors/sculpt_paint/paint_intern.h index c6fe7ed3072..caa6a456467 100644 --- a/source/blender/editors/sculpt_paint/paint_intern.h +++ b/source/blender/editors/sculpt_paint/paint_intern.h @@ -16,7 +16,16 @@ #include "DNA_scene_types.h" #ifdef __cplusplus +namespace blender { +template class BezierSpline; +} + +using BezierSpline2f = blender::BezierSpline; +using BezierSpline3f = blender::BezierSpline; extern "C" { +#else +typedef struct BezierSpline2f BezierSpline2f; +typedef struct BezierSpline3f BezierSpline3f; #endif struct ARegion; @@ -511,6 +520,13 @@ void paint_delete_blur_kernel(BlurKernel *); /* paint curve defines */ #define PAINT_CURVE_NUM_SEGMENTS 40 +void paint_stroke_spline_uv(struct PaintStroke *stroke, + struct StrokeCache *cache, + const float co[3], + float r_out[3], + float r_tan[3]); +float paint_stroke_spline_length(struct PaintStroke *stroke); + #ifdef __cplusplus } #endif diff --git a/source/blender/editors/sculpt_paint/paint_stroke.c b/source/blender/editors/sculpt_paint/paint_stroke.c deleted file mode 100644 index 97f5bd77d47..00000000000 --- a/source/blender/editors/sculpt_paint/paint_stroke.c +++ /dev/null @@ -1,1700 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later - * Copyright 2009 by Nicholas Bishop. All rights reserved. */ - -/** \file - * \ingroup edsculpt - */ - -#include "MEM_guardedalloc.h" - -#include "BLI_listbase.h" -#include "BLI_math.h" -#include "BLI_rand.h" -#include "BLI_utildefines.h" - -#include "PIL_time.h" - -#include "DNA_brush_types.h" -#include "DNA_curve_types.h" -#include "DNA_object_types.h" -#include "DNA_scene_types.h" - -#include "RNA_access.h" - -#include "BKE_brush.h" -#include "BKE_colortools.h" -#include "BKE_context.h" -#include "BKE_curve.h" -#include "BKE_image.h" -#include "BKE_paint.h" - -#include "WM_api.h" -#include "WM_types.h" - -#include "GPU_immediate.h" -#include "GPU_state.h" - -#include "ED_screen.h" -#include "ED_view3d.h" - -#include "IMB_imbuf_types.h" - -#include "paint_intern.h" -#include "sculpt_intern.h" - -#include -#include - -//#define DEBUG_TIME - -#ifdef DEBUG_TIME -# include "PIL_time_utildefines.h" -#endif - -typedef struct PaintSample { - float mouse[2]; - float pressure; -} PaintSample; - -typedef struct PaintStroke { - void *mode_data; - void *stroke_cursor; - wmTimer *timer; - struct RNG *rng; - - /* Cached values */ - ViewContext vc; - Brush *brush; - UnifiedPaintSettings *ups; - - /* used for lines and curves */ - ListBase line; - - /* Paint stroke can use up to PAINT_MAX_INPUT_SAMPLES prior inputs - * to smooth the stroke */ - PaintSample samples[PAINT_MAX_INPUT_SAMPLES]; - int num_samples; - int cur_sample; - int tot_samples; - - float last_mouse_position[2]; - float last_world_space_position[3]; - float last_scene_spacing_delta[3]; - - bool stroke_over_mesh; - /* space distance covered so far */ - float stroke_distance; - - /* Set whether any stroke step has yet occurred - * e.g. in sculpt mode, stroke doesn't start until cursor - * passes over the mesh */ - bool stroke_started; - /* Set when enough motion was found for rake rotation */ - bool rake_started; - /* event that started stroke, for modal() return */ - int event_type; - /* check if stroke variables have been initialized */ - bool stroke_init; - /* check if various brush mapping variables have been initialized */ - bool brush_init; - float initial_mouse[2]; - /* cached_pressure stores initial pressure for size pressure influence mainly */ - float cached_size_pressure; - /* last pressure will store last pressure value for use in interpolation for space strokes */ - float last_pressure; - int stroke_mode; - - float last_tablet_event_pressure; - - float zoom_2d; - int pen_flip; - - /* Tilt, as read from the event. */ - float x_tilt; - float y_tilt; - - /* line constraint */ - bool constrain_line; - float constrained_pos[2]; - - StrokeGetLocation get_location; - StrokeTestStart test_start; - StrokeUpdateStep update_step; - StrokeRedraw redraw; - StrokeDone done; - - bool original; /* Ray-cast original mesh at start of stroke. */ -} PaintStroke; - -/*** Cursors ***/ -static void paint_draw_smooth_cursor(bContext *C, int x, int y, void *customdata) -{ - Paint *paint = BKE_paint_get_active_from_context(C); - Brush *brush = BKE_paint_brush(paint); - PaintStroke *stroke = customdata; - - if (stroke && brush) { - GPU_line_smooth(true); - GPU_blend(GPU_BLEND_ALPHA); - - ARegion *region = stroke->vc.region; - - uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); - immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); - immUniformColor4ubv(paint->paint_cursor_col); - - immBegin(GPU_PRIM_LINES, 2); - immVertex2f(pos, x, y); - immVertex2f(pos, - stroke->last_mouse_position[0] + region->winrct.xmin, - stroke->last_mouse_position[1] + region->winrct.ymin); - - immEnd(); - - immUnbindProgram(); - - GPU_blend(GPU_BLEND_NONE); - GPU_line_smooth(false); - } -} - -static void paint_draw_line_cursor(bContext *C, int x, int y, void *customdata) -{ - Paint *paint = BKE_paint_get_active_from_context(C); - PaintStroke *stroke = customdata; - - GPU_line_smooth(true); - - uint shdr_pos = GPU_vertformat_attr_add( - immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); - - immBindBuiltinProgram(GPU_SHADER_3D_LINE_DASHED_UNIFORM_COLOR); - - float viewport_size[4]; - GPU_viewport_size_get_f(viewport_size); - immUniform2f("viewport_size", viewport_size[2], viewport_size[3]); - - immUniform1i("colors_len", 2); /* "advanced" mode */ - const float alpha = (float)paint->paint_cursor_col[3] / 255.0f; - immUniform4f("color", 0.0f, 0.0f, 0.0f, alpha); - immUniform4f("color2", 1.0f, 1.0f, 1.0f, alpha); - immUniform1f("dash_width", 6.0f); - immUniform1f("dash_factor", 0.5f); - - immBegin(GPU_PRIM_LINES, 2); - - ARegion *region = stroke->vc.region; - - if (stroke->constrain_line) { - immVertex2f(shdr_pos, - stroke->last_mouse_position[0] + region->winrct.xmin, - stroke->last_mouse_position[1] + region->winrct.ymin); - - immVertex2f(shdr_pos, - stroke->constrained_pos[0] + region->winrct.xmin, - stroke->constrained_pos[1] + region->winrct.ymin); - } - else { - immVertex2f(shdr_pos, - stroke->last_mouse_position[0] + region->winrct.xmin, - stroke->last_mouse_position[1] + region->winrct.ymin); - - immVertex2f(shdr_pos, x, y); - } - - immEnd(); - - immUnbindProgram(); - - GPU_line_smooth(false); -} - -static bool paint_tool_require_location(Brush *brush, ePaintMode mode) -{ - switch (mode) { - case PAINT_MODE_SCULPT: - if (ELEM(brush->sculpt_tool, - SCULPT_TOOL_GRAB, - SCULPT_TOOL_ELASTIC_DEFORM, - SCULPT_TOOL_POSE, - SCULPT_TOOL_BOUNDARY, - SCULPT_TOOL_ROTATE, - SCULPT_TOOL_SNAKE_HOOK, - SCULPT_TOOL_THUMB)) { - return false; - } - else if (SCULPT_is_cloth_deform_brush(brush)) { - return false; - } - else { - return true; - } - default: - break; - } - - return true; -} - -static bool paint_stroke_use_scene_spacing(Brush *brush, ePaintMode mode) -{ - switch (mode) { - case PAINT_MODE_SCULPT: - return brush->flag & BRUSH_SCENE_SPACING; - default: - break; - } - return false; -} - -static bool paint_tool_raycast_original(Brush *brush, ePaintMode UNUSED(mode)) -{ - return brush->flag & BRUSH_ANCHORED; -} - -static bool paint_tool_require_inbetween_mouse_events(Brush *brush, ePaintMode mode) -{ - if (brush->flag & BRUSH_ANCHORED) { - return false; - } - - switch (mode) { - case PAINT_MODE_SCULPT: - if (ELEM(brush->sculpt_tool, - SCULPT_TOOL_GRAB, - SCULPT_TOOL_ROTATE, - SCULPT_TOOL_THUMB, - SCULPT_TOOL_SNAKE_HOOK, - SCULPT_TOOL_ELASTIC_DEFORM, - SCULPT_TOOL_CLOTH, - SCULPT_TOOL_BOUNDARY, - SCULPT_TOOL_POSE)) { - return false; - } - else { - return true; - } - default: - break; - } - - return true; -} - -/* Initialize the stroke cache variants from operator properties */ -static bool paint_brush_update(bContext *C, - Brush *brush, - ePaintMode mode, - struct PaintStroke *stroke, - const float mouse_init[2], - float mouse[2], - float pressure, - float r_location[3], - bool *r_location_is_set) -{ - Scene *scene = CTX_data_scene(C); - UnifiedPaintSettings *ups = stroke->ups; - bool location_sampled = false; - bool location_success = false; - /* Use to perform all operations except applying the stroke, - * needed for operations that require cursor motion (rake). */ - bool is_dry_run = false; - bool do_random = false; - bool do_random_mask = false; - *r_location_is_set = false; - /* XXX: Use pressure value from first brush step for brushes which don't - * support strokes (grab, thumb). They depends on initial state and - * brush coord/pressure/etc. - * It's more an events design issue, which doesn't split coordinate/pressure/angle - * changing events. We should avoid this after events system re-design */ - if (!stroke->brush_init) { - copy_v2_v2(stroke->initial_mouse, mouse); - copy_v2_v2(ups->last_rake, mouse); - copy_v2_v2(ups->tex_mouse, mouse); - copy_v2_v2(ups->mask_tex_mouse, mouse); - stroke->cached_size_pressure = pressure; - - ups->do_linear_conversion = false; - ups->colorspace = NULL; - - /* check here if color sampling the main brush should do color conversion. This is done here - * to avoid locking up to get the image buffer during sampling */ - if (brush->mtex.tex && brush->mtex.tex->type == TEX_IMAGE && brush->mtex.tex->ima) { - ImBuf *tex_ibuf = BKE_image_pool_acquire_ibuf( - brush->mtex.tex->ima, &brush->mtex.tex->iuser, NULL); - if (tex_ibuf && tex_ibuf->rect_float == NULL) { - ups->do_linear_conversion = true; - ups->colorspace = tex_ibuf->rect_colorspace; - } - BKE_image_pool_release_ibuf(brush->mtex.tex->ima, tex_ibuf, NULL); - } - - stroke->brush_init = true; - } - - if (paint_supports_dynamic_size(brush, mode)) { - copy_v2_v2(ups->tex_mouse, mouse); - copy_v2_v2(ups->mask_tex_mouse, mouse); - stroke->cached_size_pressure = pressure; - } - - /* Truly temporary data that isn't stored in properties */ - - ups->stroke_active = true; - ups->size_pressure_value = stroke->cached_size_pressure; - - ups->pixel_radius = BKE_brush_size_get(scene, brush); - ups->initial_pixel_radius = BKE_brush_size_get(scene, brush); - - if (BKE_brush_use_size_pressure(brush) && paint_supports_dynamic_size(brush, mode)) { - ups->pixel_radius *= stroke->cached_size_pressure; - } - - if (paint_supports_dynamic_tex_coords(brush, mode)) { - - if (ELEM(brush->mtex.brush_map_mode, - MTEX_MAP_MODE_VIEW, - MTEX_MAP_MODE_AREA, - MTEX_MAP_MODE_RANDOM)) { - do_random = true; - } - - if (brush->mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) { - BKE_brush_randomize_texture_coords(ups, false); - } - else { - copy_v2_v2(ups->tex_mouse, mouse); - } - - /* take care of mask texture, if any */ - if (brush->mask_mtex.tex) { - - if (ELEM(brush->mask_mtex.brush_map_mode, - MTEX_MAP_MODE_VIEW, - MTEX_MAP_MODE_AREA, - MTEX_MAP_MODE_RANDOM)) { - do_random_mask = true; - } - - if (brush->mask_mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) { - BKE_brush_randomize_texture_coords(ups, true); - } - else { - copy_v2_v2(ups->mask_tex_mouse, mouse); - } - } - } - - if (brush->flag & BRUSH_ANCHORED) { - bool hit = false; - float halfway[2]; - - const float dx = mouse[0] - stroke->initial_mouse[0]; - const float dy = mouse[1] - stroke->initial_mouse[1]; - - ups->anchored_size = ups->pixel_radius = sqrtf(dx * dx + dy * dy); - - ups->brush_rotation = ups->brush_rotation_sec = atan2f(dx, dy) + (float)M_PI; - - if (brush->flag & BRUSH_EDGE_TO_EDGE) { - halfway[0] = dx * 0.5f + stroke->initial_mouse[0]; - halfway[1] = dy * 0.5f + stroke->initial_mouse[1]; - - if (stroke->get_location) { - if (stroke->get_location(C, r_location, halfway, stroke->original)) { - hit = true; - location_sampled = true; - location_success = true; - *r_location_is_set = true; - } - else if (!paint_tool_require_location(brush, mode)) { - hit = true; - } - } - else { - hit = true; - } - } - if (hit) { - copy_v2_v2(ups->anchored_initial_mouse, halfway); - copy_v2_v2(ups->tex_mouse, halfway); - copy_v2_v2(ups->mask_tex_mouse, halfway); - copy_v2_v2(mouse, halfway); - ups->anchored_size /= 2.0f; - ups->pixel_radius /= 2.0f; - stroke->stroke_distance = ups->pixel_radius; - } - else { - copy_v2_v2(ups->anchored_initial_mouse, stroke->initial_mouse); - copy_v2_v2(mouse, stroke->initial_mouse); - stroke->stroke_distance = ups->pixel_radius; - } - ups->pixel_radius /= stroke->zoom_2d; - ups->draw_anchored = true; - } - else { - /* here we are using the initial mouse coordinate because we do not want the rake - * result to depend on jittering */ - if (!stroke->brush_init) { - copy_v2_v2(ups->last_rake, mouse_init); - } - /* curve strokes do their own rake calculation */ - else if (!(brush->flag & BRUSH_CURVE)) { - if (!paint_calculate_rake_rotation(ups, brush, mouse_init)) { - /* Not enough motion to define an angle. */ - if (!stroke->rake_started) { - is_dry_run = true; - } - } - else { - stroke->rake_started = true; - } - } - } - - if ((do_random || do_random_mask) && stroke->rng == NULL) { - /* Lazy initialization. */ - uint rng_seed = (uint)(PIL_check_seconds_timer_i() & UINT_MAX); - rng_seed ^= (uint)POINTER_AS_INT(brush); - stroke->rng = BLI_rng_new(rng_seed); - } - - if (do_random) { - if (brush->mtex.brush_angle_mode & MTEX_ANGLE_RANDOM) { - ups->brush_rotation += -brush->mtex.random_angle / 2.0f + - brush->mtex.random_angle * BLI_rng_get_float(stroke->rng); - } - } - - if (do_random_mask) { - if (brush->mask_mtex.brush_angle_mode & MTEX_ANGLE_RANDOM) { - ups->brush_rotation_sec += -brush->mask_mtex.random_angle / 2.0f + - brush->mask_mtex.random_angle * BLI_rng_get_float(stroke->rng); - } - } - - if (!location_sampled) { - if (stroke->get_location) { - if (stroke->get_location(C, r_location, mouse, stroke->original)) { - location_success = true; - *r_location_is_set = true; - } - else if (!paint_tool_require_location(brush, mode)) { - location_success = true; - } - } - else { - zero_v3(r_location); - location_success = true; - /* don't set 'r_location_is_set', since we don't want to use the value. */ - } - } - - return location_success && (is_dry_run == false); -} - -static bool paint_stroke_use_dash(Brush *brush) -{ - /* Only these stroke modes support dash lines */ - return brush->flag & BRUSH_SPACE || brush->flag & BRUSH_LINE || brush->flag & BRUSH_CURVE; -} - -static bool paint_stroke_use_jitter(ePaintMode mode, Brush *brush, bool invert) -{ - bool use_jitter = (brush->flag & BRUSH_ABSOLUTE_JITTER) ? (brush->jitter_absolute != 0) : - (brush->jitter != 0); - - /* jitter-ed brush gives weird and unpredictable result for this - * kinds of stroke, so manually disable jitter usage (sergey) */ - use_jitter &= (brush->flag & (BRUSH_DRAG_DOT | BRUSH_ANCHORED)) == 0; - use_jitter &= (!ELEM(mode, PAINT_MODE_TEXTURE_2D, PAINT_MODE_TEXTURE_3D) || - !(invert && brush->imagepaint_tool == PAINT_TOOL_CLONE)); - - return use_jitter; -} - -/* Put the location of the next stroke dot into the stroke RNA and apply it to the mesh */ -static void paint_brush_stroke_add_step( - bContext *C, wmOperator *op, PaintStroke *stroke, const float mval[2], float pressure) -{ - Scene *scene = CTX_data_scene(C); - Paint *paint = BKE_paint_get_active_from_context(C); - ePaintMode mode = BKE_paintmode_get_active_from_context(C); - Brush *brush = BKE_paint_brush(paint); - UnifiedPaintSettings *ups = stroke->ups; - float mouse_out[2]; - PointerRNA itemptr; - float location[3]; - -/* the following code is adapted from texture paint. It may not be needed but leaving here - * just in case for reference (code in texpaint removed as part of refactoring). - * It's strange that only texpaint had these guards. */ -#if 0 - /* special exception here for too high pressure values on first touch in - * windows for some tablets, then we just skip first touch. */ - if (tablet && (pressure >= 0.99f) && - ((pop->s.brush->flag & BRUSH_SPACING_PRESSURE) || - BKE_brush_use_alpha_pressure(pop->s.brush) || - BKE_brush_use_size_pressure(pop->s.brush))) { - return; - } - - /* This can be removed once fixed properly in - * BKE_brush_painter_paint( - * BrushPainter *painter, BrushFunc func, - * float *pos, double time, float pressure, void *user); - * at zero pressure we should do nothing 1/2^12 is 0.0002 - * which is the sensitivity of the most sensitive pen tablet available */ - if (tablet && (pressure < 0.0002f) && - ((pop->s.brush->flag & BRUSH_SPACING_PRESSURE) || - BKE_brush_use_alpha_pressure(pop->s.brush) || - BKE_brush_use_size_pressure(pop->s.brush))) { - return; - } -#endif - - /* copy last position -before- jittering, or space fill code - * will create too many dabs */ - copy_v2_v2(stroke->last_mouse_position, mval); - stroke->last_pressure = pressure; - - if (paint_stroke_use_scene_spacing(brush, mode)) { - float world_space_position[3]; - - if (SCULPT_stroke_get_location( - C, world_space_position, stroke->last_mouse_position, stroke->original)) { - copy_v3_v3(stroke->last_world_space_position, world_space_position); - mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); - } - else { - add_v3_v3(stroke->last_world_space_position, stroke->last_scene_spacing_delta); - } - } - - if (paint_stroke_use_jitter(mode, brush, stroke->stroke_mode == BRUSH_STROKE_INVERT)) { - float delta[2]; - float factor = stroke->zoom_2d; - - if (brush->flag & BRUSH_JITTER_PRESSURE) { - factor *= pressure; - } - - BKE_brush_jitter_pos(scene, brush, mval, mouse_out); - - /* XXX: meh, this is round about because - * BKE_brush_jitter_pos isn't written in the best way to - * be reused here */ - if (factor != 1.0f) { - sub_v2_v2v2(delta, mouse_out, mval); - mul_v2_fl(delta, factor); - add_v2_v2v2(mouse_out, mval, delta); - } - } - else { - copy_v2_v2(mouse_out, mval); - } - - bool is_location_is_set; - ups->last_hit = paint_brush_update( - C, brush, mode, stroke, mval, mouse_out, pressure, location, &is_location_is_set); - if (is_location_is_set) { - copy_v3_v3(ups->last_location, location); - } - if (!ups->last_hit) { - return; - } - - /* Dash */ - bool add_step = true; - if (paint_stroke_use_dash(brush)) { - int dash_samples = stroke->tot_samples % brush->dash_samples; - float dash = (float)dash_samples / (float)brush->dash_samples; - if (dash > brush->dash_ratio) { - add_step = false; - } - } - - /* Add to stroke */ - if (add_step) { - RNA_collection_add(op->ptr, "stroke", &itemptr); - RNA_float_set(&itemptr, "size", ups->pixel_radius); - RNA_float_set_array(&itemptr, "location", location); - /* Mouse coordinates modified by the stroke type options. */ - RNA_float_set_array(&itemptr, "mouse", mouse_out); - /* Original mouse coordinates. */ - RNA_float_set_array(&itemptr, "mouse_event", mval); - RNA_boolean_set(&itemptr, "pen_flip", stroke->pen_flip); - RNA_float_set(&itemptr, "pressure", pressure); - RNA_float_set(&itemptr, "x_tilt", stroke->x_tilt); - RNA_float_set(&itemptr, "y_tilt", stroke->y_tilt); - - stroke->update_step(C, op, stroke, &itemptr); - - /* don't record this for now, it takes up a lot of memory when doing long - * strokes with small brush size, and operators have register disabled */ - RNA_collection_clear(op->ptr, "stroke"); - } - - stroke->tot_samples++; -} - -/* Returns zero if no sculpt changes should be made, non-zero otherwise */ -static bool paint_smooth_stroke(PaintStroke *stroke, - const PaintSample *sample, - ePaintMode mode, - float r_mouse[2], - float *r_pressure) -{ - if (paint_supports_smooth_stroke(stroke->brush, mode)) { - float radius = stroke->brush->smooth_stroke_radius * stroke->zoom_2d; - float u = stroke->brush->smooth_stroke_factor; - - /* If the mouse is moving within the radius of the last move, - * don't update the mouse position. This allows sharp turns. */ - if (len_squared_v2v2(stroke->last_mouse_position, sample->mouse) < square_f(radius)) { - return false; - } - - interp_v2_v2v2(r_mouse, sample->mouse, stroke->last_mouse_position, u); - *r_pressure = interpf(sample->pressure, stroke->last_pressure, u); - } - else { - r_mouse[0] = sample->mouse[0]; - r_mouse[1] = sample->mouse[1]; - *r_pressure = sample->pressure; - } - - return true; -} - -static float paint_space_stroke_spacing(bContext *C, - const Scene *scene, - PaintStroke *stroke, - float size_pressure, - float spacing_pressure) -{ - Paint *paint = BKE_paint_get_active_from_context(C); - ePaintMode mode = BKE_paintmode_get_active_from_context(C); - Brush *brush = BKE_paint_brush(paint); - float size_clamp = 0.0f; - float size = BKE_brush_size_get(scene, stroke->brush) * size_pressure; - if (paint_stroke_use_scene_spacing(brush, mode)) { - if (!BKE_brush_use_locked_size(scene, brush)) { - float last_object_space_position[3]; - mul_v3_m4v3(last_object_space_position, - stroke->vc.obact->world_to_object, - stroke->last_world_space_position); - size_clamp = paint_calc_object_space_radius(&stroke->vc, last_object_space_position, size); - } - else { - size_clamp = BKE_brush_unprojected_radius_get(scene, brush) * size_pressure; - } - } - else { - /* brushes can have a minimum size of 1.0 but with pressure it can be smaller than a pixel - * causing very high step sizes, hanging blender T32381. */ - size_clamp = max_ff(1.0f, size); - } - - float spacing = stroke->brush->spacing; - - /* apply spacing pressure */ - if (stroke->brush->flag & BRUSH_SPACING_PRESSURE) { - spacing = spacing * (1.5f - spacing_pressure); - } - - if (SCULPT_is_cloth_deform_brush(brush)) { - /* The spacing in tools that use the cloth solver should not be affected by the brush radius to - * avoid affecting the simulation update rate when changing the radius of the brush. - * With a value of 100 and the brush default of 10 for spacing, a simulation step runs every 2 - * pixels movement of the cursor. */ - size_clamp = 100.0f; - } - - /* stroke system is used for 2d paint too, so we need to account for - * the fact that brush can be scaled there. */ - spacing *= stroke->zoom_2d; - - if (paint_stroke_use_scene_spacing(brush, mode)) { - return size_clamp * spacing / 50.0f; - } - return max_ff(stroke->zoom_2d, size_clamp * spacing / 50.0f); -} - -static float paint_stroke_overlapped_curve(Brush *br, float x, float spacing) -{ - const int n = 100 / spacing; - const float h = spacing / 50.0f; - const float x0 = x - 1; - - float sum = 0; - for (int i = 0; i < n; i++) { - float xx; - - xx = fabsf(x0 + i * h); - - if (xx < 1.0f) { - sum += BKE_brush_curve_strength(br, xx, 1); - } - } - - return sum; -} - -static float paint_stroke_integrate_overlap(Brush *br, float factor) -{ - float spacing = br->spacing * factor; - - if (!(br->flag & BRUSH_SPACE_ATTEN && (br->spacing < 100))) { - return 1.0; - } - - int m = 10; - float g = 1.0f / m; - float max = 0; - for (int i = 0; i < m; i++) { - float overlap = fabs(paint_stroke_overlapped_curve(br, i * g, spacing)); - - if (overlap > max) { - max = overlap; - } - } - - if (max == 0.0f) { - return 1.0f; - } - return 1.0f / max; -} - -static float paint_space_stroke_spacing_variable(bContext *C, - const Scene *scene, - PaintStroke *stroke, - float pressure, - float dpressure, - float length) -{ - if (BKE_brush_use_size_pressure(stroke->brush)) { - /* use pressure to modify size. set spacing so that at 100%, the circles - * are aligned nicely with no overlap. for this the spacing needs to be - * the average of the previous and next size. */ - float s = paint_space_stroke_spacing(C, scene, stroke, 1.0f, pressure); - float q = s * dpressure / (2.0f * length); - float pressure_fac = (1.0f + q) / (1.0f - q); - - float last_size_pressure = stroke->last_pressure; - float new_size_pressure = stroke->last_pressure * pressure_fac; - - /* average spacing */ - float last_spacing = paint_space_stroke_spacing( - C, scene, stroke, last_size_pressure, pressure); - float new_spacing = paint_space_stroke_spacing(C, scene, stroke, new_size_pressure, pressure); - - return 0.5f * (last_spacing + new_spacing); - } - - /* no size pressure */ - return paint_space_stroke_spacing(C, scene, stroke, 1.0f, pressure); -} - -/* For brushes with stroke spacing enabled, moves mouse in steps - * towards the final mouse location. */ -static int paint_space_stroke(bContext *C, - wmOperator *op, - PaintStroke *stroke, - const float final_mouse[2], - float final_pressure) -{ - const Scene *scene = CTX_data_scene(C); - ARegion *region = CTX_wm_region(C); - UnifiedPaintSettings *ups = stroke->ups; - Paint *paint = BKE_paint_get_active_from_context(C); - ePaintMode mode = BKE_paintmode_get_active_from_context(C); - Brush *brush = BKE_paint_brush(paint); - int count = 0; - - const bool use_scene_spacing = paint_stroke_use_scene_spacing(brush, mode); - float d_world_space_position[3] = {0.0f}; - - float no_pressure_spacing = paint_space_stroke_spacing(C, scene, stroke, 1.0f, 1.0f); - float pressure = stroke->last_pressure; - float dpressure = final_pressure - stroke->last_pressure; - - float dmouse[2]; - sub_v2_v2v2(dmouse, final_mouse, stroke->last_mouse_position); - float length = normalize_v2(dmouse); - - if (use_scene_spacing) { - float world_space_position[3]; - bool hit = SCULPT_stroke_get_location(C, world_space_position, final_mouse, stroke->original); - mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position); - if (hit && stroke->stroke_over_mesh) { - sub_v3_v3v3(d_world_space_position, world_space_position, stroke->last_world_space_position); - length = len_v3(d_world_space_position); - stroke->stroke_over_mesh = true; - } - else { - length = 0.0f; - zero_v3(d_world_space_position); - stroke->stroke_over_mesh = hit; - if (stroke->stroke_over_mesh) { - copy_v3_v3(stroke->last_world_space_position, world_space_position); - } - } - } - - while (length > 0.0f) { - float spacing = paint_space_stroke_spacing_variable( - C, scene, stroke, pressure, dpressure, length); - float mouse[2]; - - if (length >= spacing) { - if (use_scene_spacing) { - float final_world_space_position[3]; - normalize_v3(d_world_space_position); - mul_v3_v3fl(final_world_space_position, d_world_space_position, spacing); - add_v3_v3v3(final_world_space_position, - stroke->last_world_space_position, - final_world_space_position); - ED_view3d_project_v2(region, final_world_space_position, mouse); - - mul_v3_v3fl(stroke->last_scene_spacing_delta, d_world_space_position, spacing); - } - else { - mouse[0] = stroke->last_mouse_position[0] + dmouse[0] * spacing; - mouse[1] = stroke->last_mouse_position[1] + dmouse[1] * spacing; - } - pressure = stroke->last_pressure + (spacing / length) * dpressure; - - ups->overlap_factor = paint_stroke_integrate_overlap(stroke->brush, - spacing / no_pressure_spacing); - - stroke->stroke_distance += spacing / stroke->zoom_2d; - paint_brush_stroke_add_step(C, op, stroke, mouse, pressure); - - length -= spacing; - pressure = stroke->last_pressure; - dpressure = final_pressure - stroke->last_pressure; - - count++; - } - else { - break; - } - } - - return count; -} - -/**** Public API ****/ - -PaintStroke *paint_stroke_new(bContext *C, - wmOperator *op, - StrokeGetLocation get_location, - StrokeTestStart test_start, - StrokeUpdateStep update_step, - StrokeRedraw redraw, - StrokeDone done, - int event_type) -{ - struct Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); - PaintStroke *stroke = MEM_callocN(sizeof(PaintStroke), "PaintStroke"); - ToolSettings *toolsettings = CTX_data_tool_settings(C); - UnifiedPaintSettings *ups = &toolsettings->unified_paint_settings; - Paint *p = BKE_paint_get_active_from_context(C); - Brush *br = stroke->brush = BKE_paint_brush(p); - RegionView3D *rv3d = CTX_wm_region_view3d(C); - float zoomx, zoomy; - - ED_view3d_viewcontext_init(C, &stroke->vc, depsgraph); - - stroke->get_location = get_location; - stroke->test_start = test_start; - stroke->update_step = update_step; - stroke->redraw = redraw; - stroke->done = done; - stroke->event_type = event_type; /* for modal, return event */ - stroke->ups = ups; - stroke->stroke_mode = RNA_enum_get(op->ptr, "mode"); - - stroke->original = paint_tool_raycast_original(br, BKE_paintmode_get_active_from_context(C)); - - get_imapaint_zoom(C, &zoomx, &zoomy); - stroke->zoom_2d = max_ff(zoomx, zoomy); - - if (stroke->stroke_mode == BRUSH_STROKE_INVERT) { - if (br->flag & BRUSH_CURVE) { - RNA_enum_set(op->ptr, "mode", BRUSH_STROKE_NORMAL); - } - } - /* initialize here */ - ups->overlap_factor = 1.0; - ups->stroke_active = true; - - if (rv3d) { - rv3d->rflag |= RV3D_PAINTING; - } - - /* Preserve location from last stroke while applying and resetting - * ups->average_stroke_counter to 1. - */ - if (ups->average_stroke_counter) { - mul_v3_fl(ups->average_stroke_accum, 1.0f / (float)ups->average_stroke_counter); - ups->average_stroke_counter = 1; - } - - /* initialize here to avoid initialization conflict with threaded strokes */ - BKE_curvemapping_init(br->curve); - if (p->flags & PAINT_USE_CAVITY_MASK) { - BKE_curvemapping_init(p->cavity_curve); - } - - BKE_paint_set_overlay_override(br->overlay_flags); - - ups->start_pixel_radius = BKE_brush_size_get(CTX_data_scene(C), br); - - return stroke; -} - -void paint_stroke_free(bContext *C, wmOperator *UNUSED(op), PaintStroke *stroke) -{ - RegionView3D *rv3d = CTX_wm_region_view3d(C); - if (rv3d) { - rv3d->rflag &= ~RV3D_PAINTING; - } - - BKE_paint_set_overlay_override(0); - - if (stroke == NULL) { - return; - } - - UnifiedPaintSettings *ups = stroke->ups; - ups->draw_anchored = false; - ups->stroke_active = false; - - if (stroke->timer) { - WM_event_remove_timer(CTX_wm_manager(C), CTX_wm_window(C), stroke->timer); - } - - if (stroke->rng) { - BLI_rng_free(stroke->rng); - } - - if (stroke->stroke_cursor) { - WM_paint_cursor_end(stroke->stroke_cursor); - } - - BLI_freelistN(&stroke->line); - - MEM_SAFE_FREE(stroke); -} - -static void stroke_done(bContext *C, wmOperator *op, PaintStroke *stroke) -{ - UnifiedPaintSettings *ups = stroke->ups; - - /* reset rotation here to avoid doing so in cursor display */ - if (!(stroke->brush->mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { - ups->brush_rotation = 0.0f; - } - - if (!(stroke->brush->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { - ups->brush_rotation_sec = 0.0f; - } - - if (stroke->stroke_started) { - if (stroke->redraw) { - stroke->redraw(C, stroke, true); - } - - if (stroke->done) { - stroke->done(C, stroke); - } - } - - paint_stroke_free(C, op, stroke); -} - -static bool curves_sculpt_brush_uses_spacing(const eBrushCurvesSculptTool tool) -{ - return ELEM(tool, CURVES_SCULPT_TOOL_ADD, CURVES_SCULPT_TOOL_DENSITY); -} - -bool paint_space_stroke_enabled(Brush *br, ePaintMode mode) -{ - if ((br->flag & BRUSH_SPACE) == 0) { - return false; - } - - if (br->sculpt_tool == SCULPT_TOOL_CLOTH || SCULPT_is_cloth_deform_brush(br)) { - /* The Cloth Brush is a special case for stroke spacing. Even if it has grab modes which do - * not support dynamic size, stroke spacing needs to be enabled so it is possible to control - * whether the simulation runs constantly or only when the brush moves when using the cloth - * grab brushes. */ - return true; - } - - if (mode == PAINT_MODE_SCULPT_CURVES && - !curves_sculpt_brush_uses_spacing(br->curves_sculpt_tool)) { - return false; - } - - return paint_supports_dynamic_size(br, mode); -} - -static bool sculpt_is_grab_tool(Brush *br) -{ - - if (br->sculpt_tool == SCULPT_TOOL_CLOTH && br->cloth_deform_type == BRUSH_CLOTH_DEFORM_GRAB) { - return true; - } - return ELEM(br->sculpt_tool, - SCULPT_TOOL_GRAB, - SCULPT_TOOL_ELASTIC_DEFORM, - SCULPT_TOOL_POSE, - SCULPT_TOOL_BOUNDARY, - SCULPT_TOOL_THUMB, - SCULPT_TOOL_ROTATE, - SCULPT_TOOL_SNAKE_HOOK); -} - -bool paint_supports_dynamic_size(Brush *br, ePaintMode mode) -{ - if (br->flag & BRUSH_ANCHORED) { - return false; - } - - switch (mode) { - case PAINT_MODE_SCULPT: - if (sculpt_is_grab_tool(br)) { - return false; - } - break; - - case PAINT_MODE_TEXTURE_2D: /* fall through */ - case PAINT_MODE_TEXTURE_3D: - if ((br->imagepaint_tool == PAINT_TOOL_FILL) && (br->flag & BRUSH_USE_GRADIENT)) { - return false; - } - break; - - default: - break; - } - return true; -} - -bool paint_supports_smooth_stroke(Brush *br, ePaintMode mode) -{ - if (!(br->flag & BRUSH_SMOOTH_STROKE) || - (br->flag & (BRUSH_ANCHORED | BRUSH_DRAG_DOT | BRUSH_LINE))) { - return false; - } - - switch (mode) { - case PAINT_MODE_SCULPT: - if (sculpt_is_grab_tool(br)) { - return false; - } - break; - default: - break; - } - return true; -} - -bool paint_supports_texture(ePaintMode mode) -{ - /* omit: PAINT_WEIGHT, PAINT_SCULPT_UV, PAINT_INVALID */ - return ELEM( - mode, PAINT_MODE_SCULPT, PAINT_MODE_VERTEX, PAINT_MODE_TEXTURE_3D, PAINT_MODE_TEXTURE_2D); -} - -bool paint_supports_dynamic_tex_coords(Brush *br, ePaintMode mode) -{ - if (br->flag & BRUSH_ANCHORED) { - return false; - } - - switch (mode) { - case PAINT_MODE_SCULPT: - if (sculpt_is_grab_tool(br)) { - return false; - } - break; - default: - break; - } - return true; -} - -#define PAINT_STROKE_MODAL_CANCEL 1 - -struct wmKeyMap *paint_stroke_modal_keymap(struct wmKeyConfig *keyconf) -{ - static struct EnumPropertyItem modal_items[] = { - {PAINT_STROKE_MODAL_CANCEL, "CANCEL", 0, "Cancel", "Cancel and undo a stroke in progress"}, - - {0}}; - - static const char *name = "Paint Stroke Modal"; - - struct wmKeyMap *keymap = WM_modalkeymap_find(keyconf, name); - - /* This function is called for each space-type, only needs to add map once. */ - if (!keymap) { - keymap = WM_modalkeymap_ensure(keyconf, name, modal_items); - } - - return keymap; -} - -static void paint_stroke_add_sample( - const Paint *paint, PaintStroke *stroke, float x, float y, float pressure) -{ - PaintSample *sample = &stroke->samples[stroke->cur_sample]; - int max_samples = CLAMPIS(paint->num_input_samples, 1, PAINT_MAX_INPUT_SAMPLES); - - sample->mouse[0] = x; - sample->mouse[1] = y; - sample->pressure = pressure; - - stroke->cur_sample++; - if (stroke->cur_sample >= max_samples) { - stroke->cur_sample = 0; - } - if (stroke->num_samples < max_samples) { - stroke->num_samples++; - } -} - -static void paint_stroke_sample_average(const PaintStroke *stroke, PaintSample *average) -{ - memset(average, 0, sizeof(*average)); - - BLI_assert(stroke->num_samples > 0); - - for (int i = 0; i < stroke->num_samples; i++) { - add_v2_v2(average->mouse, stroke->samples[i].mouse); - average->pressure += stroke->samples[i].pressure; - } - - mul_v2_fl(average->mouse, 1.0f / stroke->num_samples); - average->pressure /= stroke->num_samples; - - // printf("avg=(%f, %f), num=%d\n", average->mouse[0], average->mouse[1], stroke->num_samples); -} - -/** - * Slightly different version of spacing for line/curve strokes, - * makes sure the dabs stay on the line path. - */ -static void paint_line_strokes_spacing(bContext *C, - wmOperator *op, - PaintStroke *stroke, - float spacing, - float *length_residue, - const float old_pos[2], - const float new_pos[2]) -{ - UnifiedPaintSettings *ups = stroke->ups; - Paint *paint = BKE_paint_get_active_from_context(C); - Brush *brush = BKE_paint_brush(paint); - ePaintMode mode = BKE_paintmode_get_active_from_context(C); - ARegion *region = CTX_wm_region(C); - - const bool use_scene_spacing = paint_stroke_use_scene_spacing(brush, mode); - - float mouse[2], dmouse[2]; - float length; - float d_world_space_position[3] = {0.0f}; - float world_space_position_old[3], world_space_position_new[3]; - - copy_v2_v2(stroke->last_mouse_position, old_pos); - - if (use_scene_spacing) { - bool hit_old = SCULPT_stroke_get_location( - C, world_space_position_old, old_pos, stroke->original); - bool hit_new = SCULPT_stroke_get_location( - C, world_space_position_new, new_pos, stroke->original); - mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position_old); - mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position_new); - if (hit_old && hit_new && stroke->stroke_over_mesh) { - sub_v3_v3v3(d_world_space_position, world_space_position_new, world_space_position_old); - length = len_v3(d_world_space_position); - stroke->stroke_over_mesh = true; - } - else { - length = 0.0f; - zero_v3(d_world_space_position); - stroke->stroke_over_mesh = hit_new; - if (stroke->stroke_over_mesh) { - copy_v3_v3(stroke->last_world_space_position, world_space_position_old); - } - } - } - else { - sub_v2_v2v2(dmouse, new_pos, old_pos); - length = normalize_v2(dmouse); - } - - BLI_assert(length >= 0.0f); - - if (length == 0.0f) { - return; - } - - while (length > 0.0f) { - float spacing_final = spacing - *length_residue; - length += *length_residue; - *length_residue = 0.0; - - if (length >= spacing) { - if (use_scene_spacing) { - float final_world_space_position[3]; - normalize_v3(d_world_space_position); - mul_v3_v3fl(final_world_space_position, d_world_space_position, spacing_final); - add_v3_v3v3( - final_world_space_position, world_space_position_old, final_world_space_position); - ED_view3d_project_v2(region, final_world_space_position, mouse); - } - else { - mouse[0] = stroke->last_mouse_position[0] + dmouse[0] * spacing_final; - mouse[1] = stroke->last_mouse_position[1] + dmouse[1] * spacing_final; - } - - ups->overlap_factor = paint_stroke_integrate_overlap(stroke->brush, 1.0); - - stroke->stroke_distance += spacing / stroke->zoom_2d; - paint_brush_stroke_add_step(C, op, stroke, mouse, 1.0); - - length -= spacing; - spacing_final = spacing; - } - else { - break; - } - } - - *length_residue = length; -} - -static void paint_stroke_line_end(bContext *C, - wmOperator *op, - PaintStroke *stroke, - const float mouse[2]) -{ - Brush *br = stroke->brush; - if (stroke->stroke_started && (br->flag & BRUSH_LINE)) { - stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); - - paint_brush_stroke_add_step(C, op, stroke, stroke->last_mouse_position, 1.0); - paint_space_stroke(C, op, stroke, mouse, 1.0); - } -} - -static bool paint_stroke_curve_end(bContext *C, wmOperator *op, PaintStroke *stroke) -{ - Brush *br = stroke->brush; - - if (br->flag & BRUSH_CURVE) { - UnifiedPaintSettings *ups = &CTX_data_tool_settings(C)->unified_paint_settings; - const Scene *scene = CTX_data_scene(C); - const float spacing = paint_space_stroke_spacing(C, scene, stroke, 1.0f, 1.0f); - PaintCurve *pc = br->paint_curve; - PaintCurvePoint *pcp; - float length_residue = 0.0f; - int i; - - if (!pc) { - return true; - } - -#ifdef DEBUG_TIME - TIMEIT_START_AVERAGED(whole_stroke); -#endif - - pcp = pc->points; - stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); - - for (i = 0; i < pc->tot_points - 1; i++, pcp++) { - int j; - float data[(PAINT_CURVE_NUM_SEGMENTS + 1) * 2]; - float tangents[(PAINT_CURVE_NUM_SEGMENTS + 1) * 2]; - PaintCurvePoint *pcp_next = pcp + 1; - bool do_rake = false; - - for (j = 0; j < 2; j++) { - BKE_curve_forward_diff_bezier(pcp->bez.vec[1][j], - pcp->bez.vec[2][j], - pcp_next->bez.vec[0][j], - pcp_next->bez.vec[1][j], - data + j, - PAINT_CURVE_NUM_SEGMENTS, - sizeof(float[2])); - } - - if ((br->mtex.brush_angle_mode & MTEX_ANGLE_RAKE) || - (br->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { - do_rake = true; - for (j = 0; j < 2; j++) { - BKE_curve_forward_diff_tangent_bezier(pcp->bez.vec[1][j], - pcp->bez.vec[2][j], - pcp_next->bez.vec[0][j], - pcp_next->bez.vec[1][j], - tangents + j, - PAINT_CURVE_NUM_SEGMENTS, - sizeof(float[2])); - } - } - - for (j = 0; j < PAINT_CURVE_NUM_SEGMENTS; j++) { - if (do_rake) { - float rotation = atan2f(tangents[2 * j], tangents[2 * j + 1]); - paint_update_brush_rake_rotation(ups, br, rotation); - } - - if (!stroke->stroke_started) { - stroke->last_pressure = 1.0; - copy_v2_v2(stroke->last_mouse_position, data + 2 * j); - - if (paint_stroke_use_scene_spacing(br, BKE_paintmode_get_active_from_context(C))) { - stroke->stroke_over_mesh = SCULPT_stroke_get_location( - C, stroke->last_world_space_position, data + 2 * j, stroke->original); - mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); - } - - stroke->stroke_started = stroke->test_start(C, op, stroke->last_mouse_position); - - if (stroke->stroke_started) { - paint_brush_stroke_add_step(C, op, stroke, data + 2 * j, 1.0); - paint_line_strokes_spacing( - C, op, stroke, spacing, &length_residue, data + 2 * j, data + 2 * (j + 1)); - } - } - else { - paint_line_strokes_spacing( - C, op, stroke, spacing, &length_residue, data + 2 * j, data + 2 * (j + 1)); - } - } - } - - stroke_done(C, op, stroke); - -#ifdef DEBUG_TIME - TIMEIT_END_AVERAGED(whole_stroke); -#endif - - return true; - } - - return false; -} - -static void paint_stroke_line_constrain(PaintStroke *stroke, float mouse[2]) -{ - if (stroke->constrain_line) { - float line[2]; - float angle, len, res; - - sub_v2_v2v2(line, mouse, stroke->last_mouse_position); - angle = atan2f(line[1], line[0]); - len = len_v2(line); - - /* divide angle by PI/4 */ - angle = 4.0f * angle / (float)M_PI; - - /* now take residue */ - res = angle - floorf(angle); - - /* residue decides how close we are at a certain angle */ - if (res <= 0.5f) { - angle = floorf(angle) * (float)M_PI_4; - } - else { - angle = (floorf(angle) + 1.0f) * (float)M_PI_4; - } - - mouse[0] = stroke->constrained_pos[0] = len * cosf(angle) + stroke->last_mouse_position[0]; - mouse[1] = stroke->constrained_pos[1] = len * sinf(angle) + stroke->last_mouse_position[1]; - } -} - -int paint_stroke_modal(bContext *C, wmOperator *op, const wmEvent *event, PaintStroke **stroke_p) -{ - Paint *p = BKE_paint_get_active_from_context(C); - ePaintMode mode = BKE_paintmode_get_active_from_context(C); - PaintStroke *stroke = *stroke_p; - Brush *br = stroke->brush = BKE_paint_brush(p); - PaintSample sample_average; - float mouse[2]; - bool first_dab = false; - bool first_modal = false; - bool redraw = false; - float pressure; - - if (event->type == INBETWEEN_MOUSEMOVE && !paint_tool_require_inbetween_mouse_events(br, mode)) { - return OPERATOR_RUNNING_MODAL; - } - - /* see if tablet affects event. Line, anchored and drag dot strokes do not support pressure */ - pressure = ((br->flag & (BRUSH_LINE | BRUSH_ANCHORED | BRUSH_DRAG_DOT)) ? - 1.0f : - WM_event_tablet_data(event, &stroke->pen_flip, NULL)); - - /* When processing a timer event the pressure from the event is 0, so use the last valid - * pressure. */ - if (event->type == TIMER) { - pressure = stroke->last_tablet_event_pressure; - } - else { - stroke->last_tablet_event_pressure = pressure; - } - - paint_stroke_add_sample(p, stroke, event->mval[0], event->mval[1], pressure); - paint_stroke_sample_average(stroke, &sample_average); - - /* Tilt. */ - if (WM_event_is_tablet(event)) { - stroke->x_tilt = event->tablet.x_tilt; - stroke->y_tilt = event->tablet.y_tilt; - } - -#ifdef WITH_INPUT_NDOF - /* let NDOF motion pass through to the 3D view so we can paint and rotate simultaneously! - * this isn't perfect... even when an extra MOUSEMOVE is spoofed, the stroke discards it - * since the 2D deltas are zero -- code in this file needs to be updated to use the - * post-NDOF_MOTION MOUSEMOVE */ - if (event->type == NDOF_MOTION) { - return OPERATOR_PASS_THROUGH; - } -#endif - - /* one time initialization */ - if (!stroke->stroke_init) { - if (paint_stroke_curve_end(C, op, stroke)) { - *stroke_p = NULL; - return OPERATOR_FINISHED; - } - - if (paint_supports_smooth_stroke(br, mode)) { - stroke->stroke_cursor = WM_paint_cursor_activate( - SPACE_TYPE_ANY, RGN_TYPE_ANY, PAINT_brush_tool_poll, paint_draw_smooth_cursor, stroke); - } - - stroke->stroke_init = true; - first_modal = true; - } - - /* one time stroke initialization */ - if (!stroke->stroke_started) { - stroke->last_pressure = sample_average.pressure; - copy_v2_v2(stroke->last_mouse_position, sample_average.mouse); - if (paint_stroke_use_scene_spacing(br, mode)) { - stroke->stroke_over_mesh = SCULPT_stroke_get_location( - C, stroke->last_world_space_position, sample_average.mouse, stroke->original); - mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); - } - stroke->stroke_started = stroke->test_start(C, op, sample_average.mouse); - BLI_assert((stroke->stroke_started & ~1) == 0); /* 0/1 */ - - if (stroke->stroke_started) { - if (br->flag & BRUSH_AIRBRUSH) { - stroke->timer = WM_event_add_timer( - CTX_wm_manager(C), CTX_wm_window(C), TIMER, stroke->brush->rate); - } - - if (br->flag & BRUSH_LINE) { - stroke->stroke_cursor = WM_paint_cursor_activate( - SPACE_TYPE_ANY, RGN_TYPE_ANY, PAINT_brush_tool_poll, paint_draw_line_cursor, stroke); - } - - first_dab = true; - } - } - - /* Cancel */ - if (event->type == EVT_MODAL_MAP && event->val == PAINT_STROKE_MODAL_CANCEL) { - if (op->type->cancel) { - op->type->cancel(C, op); - } - else { - paint_stroke_cancel(C, op, stroke); - } - return OPERATOR_CANCELLED; - } - - if (event->type == stroke->event_type && !first_modal) { - if (event->val == KM_RELEASE) { - copy_v2_fl2(mouse, event->mval[0], event->mval[1]); - paint_stroke_line_constrain(stroke, mouse); - paint_stroke_line_end(C, op, stroke, mouse); - stroke_done(C, op, stroke); - *stroke_p = NULL; - return OPERATOR_FINISHED; - } - } - else if (ELEM(event->type, EVT_RETKEY, EVT_SPACEKEY)) { - paint_stroke_line_end(C, op, stroke, sample_average.mouse); - stroke_done(C, op, stroke); - *stroke_p = NULL; - return OPERATOR_FINISHED; - } - else if (br->flag & BRUSH_LINE) { - if (event->modifier & KM_ALT) { - stroke->constrain_line = true; - } - else { - stroke->constrain_line = false; - } - - copy_v2_fl2(mouse, event->mval[0], event->mval[1]); - paint_stroke_line_constrain(stroke, mouse); - - if (stroke->stroke_started && (first_modal || ISMOUSE_MOTION(event->type))) { - if ((br->mtex.brush_angle_mode & MTEX_ANGLE_RAKE) || - (br->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { - copy_v2_v2(stroke->ups->last_rake, stroke->last_mouse_position); - } - paint_calculate_rake_rotation(stroke->ups, br, mouse); - } - } - else if (first_modal || - /* regular dabs */ - (!(br->flag & BRUSH_AIRBRUSH) && ISMOUSE_MOTION(event->type)) || - /* airbrush */ - ((br->flag & BRUSH_AIRBRUSH) && event->type == TIMER && - event->customdata == stroke->timer)) { - if (paint_smooth_stroke(stroke, &sample_average, mode, mouse, &pressure)) { - if (stroke->stroke_started) { - if (paint_space_stroke_enabled(br, mode)) { - if (paint_space_stroke(C, op, stroke, mouse, pressure)) { - redraw = true; - } - } - else { - float dmouse[2]; - sub_v2_v2v2(dmouse, mouse, stroke->last_mouse_position); - stroke->stroke_distance += len_v2(dmouse); - paint_brush_stroke_add_step(C, op, stroke, mouse, pressure); - redraw = true; - } - } - } - } - - /* we want the stroke to have the first daub at the start location - * instead of waiting till we have moved the space distance */ - if (first_dab && paint_space_stroke_enabled(br, mode) && !(br->flag & BRUSH_SMOOTH_STROKE)) { - stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); - paint_brush_stroke_add_step(C, op, stroke, sample_average.mouse, sample_average.pressure); - redraw = true; - } - - /* do updates for redraw. if event is in between mouse-move there are more - * coming, so postpone potentially slow redraw updates until all are done */ - if (event->type != INBETWEEN_MOUSEMOVE) { - wmWindow *window = CTX_wm_window(C); - ARegion *region = CTX_wm_region(C); - - /* At the very least, invalidate the cursor */ - if (region && (p->flags & PAINT_SHOW_BRUSH)) { - WM_paint_cursor_tag_redraw(window, region); - } - - if (redraw && stroke->redraw) { - stroke->redraw(C, stroke, false); - } - } - - return OPERATOR_RUNNING_MODAL; -} - -int paint_stroke_exec(bContext *C, wmOperator *op, PaintStroke *stroke) -{ - /* only when executed for the first time */ - if (stroke->stroke_started == 0) { - PropertyRNA *strokeprop; - PointerRNA firstpoint; - float mouse[2]; - - strokeprop = RNA_struct_find_property(op->ptr, "stroke"); - - if (RNA_property_collection_lookup_int(op->ptr, strokeprop, 0, &firstpoint)) { - RNA_float_get_array(&firstpoint, "mouse", mouse); - stroke->stroke_started = stroke->test_start(C, op, mouse); - } - } - - if (stroke->stroke_started) { - RNA_BEGIN (op->ptr, itemptr, "stroke") { - stroke->update_step(C, op, stroke, &itemptr); - } - RNA_END; - } - - bool ok = (stroke->stroke_started != 0); - - stroke_done(C, op, stroke); - - return ok ? OPERATOR_FINISHED : OPERATOR_CANCELLED; -} - -void paint_stroke_cancel(bContext *C, wmOperator *op, PaintStroke *stroke) -{ - stroke_done(C, op, stroke); -} - -ViewContext *paint_stroke_view_context(PaintStroke *stroke) -{ - return &stroke->vc; -} - -void *paint_stroke_mode_data(struct PaintStroke *stroke) -{ - return stroke->mode_data; -} - -bool paint_stroke_flipped(struct PaintStroke *stroke) -{ - return stroke->pen_flip; -} - -bool paint_stroke_inverted(struct PaintStroke *stroke) -{ - return stroke->stroke_mode == BRUSH_STROKE_INVERT; -} - -float paint_stroke_distance_get(struct PaintStroke *stroke) -{ - return stroke->stroke_distance; -} - -void paint_stroke_set_mode_data(PaintStroke *stroke, void *mode_data) -{ - stroke->mode_data = mode_data; -} - -bool paint_stroke_started(PaintStroke *stroke) -{ - return stroke->stroke_started; -} - -bool PAINT_brush_tool_poll(bContext *C) -{ - Paint *p = BKE_paint_get_active_from_context(C); - Object *ob = CTX_data_active_object(C); - ScrArea *area = CTX_wm_area(C); - ARegion *region = CTX_wm_region(C); - - if (p && ob && BKE_paint_brush(p) && - (area && ELEM(area->spacetype, SPACE_VIEW3D, SPACE_IMAGE)) && - (region && region->regiontype == RGN_TYPE_WINDOW)) { - /* Check the current tool is a brush. */ - bToolRef *tref = area->runtime.tool; - if (tref && tref->runtime && tref->runtime->data_block[0]) { - return true; - } - } - return false; -} diff --git a/source/blender/editors/sculpt_paint/paint_stroke.cc b/source/blender/editors/sculpt_paint/paint_stroke.cc new file mode 100644 index 00000000000..99fbc92d7e1 --- /dev/null +++ b/source/blender/editors/sculpt_paint/paint_stroke.cc @@ -0,0 +1,2065 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2009 by Nicholas Bishop. All rights reserved. */ + +/** \file + * \ingroup edsculpt + */ + +#include "MEM_guardedalloc.h" + +#include "BLI_even_spline.hh" +#include "BLI_listbase.h" +#include "BLI_math.h" +#include "BLI_math_vec_types.hh" +#include "BLI_rand.h" +#include "BLI_utildefines.h" + +#include "PIL_time.h" + +#include "DNA_brush_types.h" +#include "DNA_curve_types.h" +#include "DNA_object_types.h" +#include "DNA_scene_types.h" + +#include "RNA_access.h" + +#include "BKE_brush.h" +#include "BKE_colortools.h" +#include "BKE_context.h" +#include "BKE_curve.h" +#include "BKE_image.h" +#include "BKE_paint.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "GPU_immediate.h" +#include "GPU_state.h" + +#include "ED_screen.h" +#include "ED_space_api.h" +#include "ED_view3d.h" + +#include "IMB_imbuf_types.h" + +#include "paint_intern.h" +#include "sculpt_intern.h" + +#include +#include + +//#define DEBUG_TIME +#define DRAW_DEBUG_VIS + +using blender::float2; +using blender::float3; + +#ifdef DEBUG_TIME +# include "PIL_time_utildefines.h" +#endif + +typedef struct PaintSample { + float mouse[2]; + float pressure; +} PaintSample; + +typedef struct PaintStrokePoint { + float mouse_in[2], mouse_out[2]; + float location[3]; + float pressure, x_tilt, y_tilt; + bool pen_flip; + float size; +} PaintStrokePoint; + +typedef struct PaintStroke { + void *mode_data; + void *stroke_cursor; + wmTimer *timer; + struct RNG *rng; + + /* Cached values */ + ViewContext vc; + Brush *brush; + UnifiedPaintSettings *ups; + + /* used for lines and curves */ + ListBase line; + + bool need_roll_mapping; + int stroke_sample_index; + + /* Paint stroke can use up to PAINT_MAX_INPUT_SAMPLES prior inputs + * to smooth the stroke */ + PaintSample samples[PAINT_MAX_INPUT_SAMPLES]; + int num_samples; + int cur_sample; + int tot_samples; + + PaintStrokePoint points[PAINT_MAX_INPUT_SAMPLES]; + int num_points; + int cur_point; + int tot_points; + + BezierSpline2f *spline; + BezierSpline3f *world_spline; + + float last_mouse_position[2]; + float last_world_space_position[3]; + float last_scene_spacing_delta[3]; + + bool stroke_over_mesh; + /* space distance covered so far */ + float stroke_distance; + float stroke_distance_world; + + /* Set whether any stroke step has yet occurred + * e.g. in sculpt mode, stroke doesn't start until cursor + * passes over the mesh */ + bool stroke_started; + /* Set when enough motion was found for rake rotation */ + bool rake_started; + /* event that started stroke, for modal() return */ + int event_type; + /* check if stroke variables have been initialized */ + bool stroke_init; + /* check if various brush mapping variables have been initialized */ + bool brush_init; + float initial_mouse[2]; + /* cached_pressure stores initial pressure for size pressure influence mainly */ + float cached_size_pressure; + /* last pressure will store last pressure value for use in interpolation for space strokes */ + float last_pressure; + int stroke_mode; + + float spacing_raw; + + float last_tablet_event_pressure; + + float zoom_2d; + int pen_flip; + + /* Tilt, as read from the event. */ + float x_tilt; + float y_tilt; + + /* line constraint */ + bool constrain_line; + float constrained_pos[2]; + + StrokeGetLocation get_location; + StrokeTestStart test_start; + StrokeUpdateStep update_step; + StrokeRedraw redraw; + StrokeDone done; + + bool original; /* Ray-cast original mesh at start of stroke. */ + void *debug_draw_handle; +} PaintStroke; + +static int paint_stroke_max_points(const Paint *paint, PaintStroke *stroke) +{ + if (!stroke->need_roll_mapping) { + return 1; + } + + float s = max_ff(stroke->spacing_raw, 0.05); + + int tot = (int)ceilf(1.0f / s) + 2; + tot = max_ii(tot, 5); + + return tot; +} + +static void paint_stroke_add_point(const Paint *paint, + PaintStroke *stroke, + const float mouse_in[2], + const float mouse_out[2], + const float loc[3], + float size, + float pressure, + bool pen_flip, + float x_tilt, + float y_tilt) +{ + PaintStrokePoint *point = &stroke->points[stroke->cur_point]; + int max_points = paint_stroke_max_points(paint, stroke); + + point->size = size; + copy_v2_v2(point->mouse_in, mouse_in); + copy_v2_v2(point->mouse_out, mouse_out); + point->x_tilt = x_tilt; + point->y_tilt = y_tilt; + point->pen_flip = pen_flip; + copy_v3_v3(point->location, loc); + point->pressure = pressure; + + stroke->cur_point++; + if (stroke->cur_point >= max_points) { + stroke->cur_point = 0; + } + if (stroke->num_points < max_points) { + stroke->num_points++; + } +} + +static void paint_project_cubic(bContext *C, + PaintStroke *stroke, + blender::CubicBezier &bezier2d, + blender::CubicBezier &bezier3d) +{ + float2 mvals[4]; + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 2; j++) { + mvals[i][j] = bezier2d.ps[i][j]; + } + } + + for (int i = 0; i < 4; i++) { + if (!SCULPT_stroke_get_location(C, bezier3d.ps[i], mvals[i], true)) { + ED_view3d_win_to_3d(CTX_wm_view3d(C), + CTX_wm_region(C), + stroke->last_world_space_position, + mvals[i], + bezier3d.ps[i]); + } + } + + bezier3d.update(); +} + +static void paint_brush_make_spline(bContext *C, PaintStroke *stroke) +{ + float a[2], b[2], c[2], d[2]; + + if (stroke->num_points < 4) { + return; + } + + int cur = (stroke->cur_point - 1 + stroke->num_points) % stroke->num_points; + + int ia = (cur - 2 + stroke->num_points) % stroke->num_points; + int id = (cur - 1 + stroke->num_points) % stroke->num_points; + + int ib = (cur - 3 + stroke->num_points) % stroke->num_points; + int ic = (cur - 0 + stroke->num_points) % stroke->num_points; + + copy_v2_v2(a, stroke->points[ia].mouse_out); + copy_v2_v2(b, stroke->points[ib].mouse_out); + copy_v2_v2(c, stroke->points[ic].mouse_out); + copy_v2_v2(d, stroke->points[id].mouse_out); + + float scale = 1.0; + scale /= 3.0f; + + float tmp1[2]; + float tmp2[2]; +#if 1 + sub_v2_v2v2(tmp1, d, a); + sub_v2_v2v2(tmp2, a, b); + interp_v2_v2v2(b, tmp1, tmp2, 0.5f); + mul_v2_fl(b, scale); +#else + zero_v2(b); +#endif + + add_v2_v2(b, a); + +#if 1 + sub_v2_v2v2(tmp1, a, d); + sub_v2_v2v2(tmp2, d, c); + interp_v2_v2v2(c, tmp1, tmp2, 0.5f); + mul_v2_fl(c, scale); +#else + zero_v2(c); +#endif + + add_v2_v2(c, d); +#if 0 + printf("\n"); + printf("a: %.2f: %.2f\n", a[0], a[1]); + printf("b: %.2f: %.2f\n", b[0], b[1]); + printf("c: %.2f: %.2f\n", c[0], c[1]); + printf("d: %.2f: %.2f\n", d[0], d[1]); +#endif + + blender::CubicBezier bez(a, b, c, d); + bez.update(); + stroke->spline->add(bez); + + blender::CubicBezier bez3d; + paint_project_cubic(C, stroke, bez, bez3d); + bez3d.update(); + + stroke->world_spline->add(bez3d); + + while (stroke->spline->segments.size() > paint_stroke_max_points(nullptr, stroke) + 1) { + stroke->spline->pop_front(); + } + + while (stroke->world_spline->segments.size() > paint_stroke_max_points(nullptr, stroke) + 1) { + stroke->stroke_distance_world += stroke->world_spline->segments[0].bezier.length; + stroke->world_spline->pop_front(); + } +} + +#ifdef DRAW_DEBUG_VIS +static void paint_brush_cubic_vis(const bContext *C, ARegion *region, void *userdata) +{ + PaintStroke *stroke = (PaintStroke *)userdata; + Object *ob = CTX_data_active_object(C); + + if (!ob || !ob->sculpt || !ob->sculpt->cache || !stroke->world_spline) { + return; + } + + GPU_line_smooth(false); + GPU_depth_test(GPU_DEPTH_NONE); + GPU_depth_mask(false); + GPU_blend(GPU_BLEND_ALPHA); + + uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT); + immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); // GPU_SHADER_3D_POLYLINE_UNIFORM_COLOR); + immUniformColor4ub(255, 150, 0, 185); + + int steps = 256; + + immUniformColor4ub(45, 75, 255, 255); + + immBegin(GPU_PRIM_LINE_STRIP, steps); + + float s = 0.0f; + float ds = stroke->world_spline->length / (steps - 1); + + for (int i = 0; i < steps; i++, s += ds) { + float3 co = stroke->world_spline->evaluate(s); + + mul_v3_m4v3(co, ob->object_to_world, co); + immVertex3fv(pos, co); + } + immEnd(); + + s = 0.0f; + ds = 0.1f; + steps = (int)floorf(stroke->world_spline->length / ds + 0.5f); + + immUniformColor4ub(255, 0, 0, 170); + immBegin(GPU_PRIM_POINTS, steps); + for (int i = 0; i < steps; i++, s += ds) { + float3 co = stroke->world_spline->evaluate(s); + mul_v3_m4v3(co, ob->object_to_world, co); + + immVertex3fv(pos, co); + } + + immEnd(); + + immUniformColor4ub(0, 255, 25, 55); + for (int is_points = 0; is_points < 2; is_points++) { + immBegin(is_points ? GPU_PRIM_POINTS : GPU_PRIM_LINE_STRIP, stroke->num_points); + for (int i = 0; i < stroke->num_points; i++) { + int idx = (i + stroke->cur_point) % stroke->num_points; + float3 co = stroke->points[idx].location; + mul_v3_m4v3(co, ob->object_to_world, co); + + immVertex3fv(pos, co); + } + immEnd(); + } + + immUnbindProgram(); + + GPU_blend(GPU_BLEND_NONE); + GPU_line_smooth(false); +} +#endif + +/*** Cursors ***/ +static void paint_draw_smooth_cursor(bContext *C, int x, int y, void *customdata) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + PaintStroke *stroke = (PaintStroke *)customdata; + + if (stroke && brush) { + GPU_line_smooth(true); + GPU_blend(GPU_BLEND_ALPHA); + + ARegion *region = stroke->vc.region; + + uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); + immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); + immUniformColor4ubv(paint->paint_cursor_col); + + immBegin(GPU_PRIM_LINES, 2); + immVertex2f(pos, x, y); + immVertex2f(pos, + stroke->last_mouse_position[0] + region->winrct.xmin, + stroke->last_mouse_position[1] + region->winrct.ymin); + + immEnd(); + + immUnbindProgram(); + + GPU_blend(GPU_BLEND_NONE); + GPU_line_smooth(false); + } +} + +static void paint_draw_line_cursor(bContext *C, int x, int y, void *customdata) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + PaintStroke *stroke = (PaintStroke *)customdata; + + GPU_line_smooth(true); + + uint shdr_pos = GPU_vertformat_attr_add( + immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); + + immBindBuiltinProgram(GPU_SHADER_3D_LINE_DASHED_UNIFORM_COLOR); + + float viewport_size[4]; + GPU_viewport_size_get_f(viewport_size); + immUniform2f("viewport_size", viewport_size[2], viewport_size[3]); + + immUniform1i("colors_len", 2); /* "advanced" mode */ + const float alpha = (float)paint->paint_cursor_col[3] / 255.0f; + immUniform4f("color", 0.0f, 0.0f, 0.0f, alpha); + immUniform4f("color2", 1.0f, 1.0f, 1.0f, alpha); + immUniform1f("dash_width", 6.0f); + immUniform1f("dash_factor", 0.5f); + + immBegin(GPU_PRIM_LINES, 2); + + ARegion *region = stroke->vc.region; + + if (stroke->constrain_line) { + immVertex2f(shdr_pos, + stroke->last_mouse_position[0] + region->winrct.xmin, + stroke->last_mouse_position[1] + region->winrct.ymin); + + immVertex2f(shdr_pos, + stroke->constrained_pos[0] + region->winrct.xmin, + stroke->constrained_pos[1] + region->winrct.ymin); + } + else { + immVertex2f(shdr_pos, + stroke->last_mouse_position[0] + region->winrct.xmin, + stroke->last_mouse_position[1] + region->winrct.ymin); + + immVertex2f(shdr_pos, x, y); + } + + immEnd(); + + immUnbindProgram(); + + GPU_line_smooth(false); +} + +static bool paint_tool_require_location(Brush *brush, ePaintMode mode) +{ + switch (mode) { + case PAINT_MODE_SCULPT: + if (ELEM(brush->sculpt_tool, + SCULPT_TOOL_GRAB, + SCULPT_TOOL_ELASTIC_DEFORM, + SCULPT_TOOL_POSE, + SCULPT_TOOL_BOUNDARY, + SCULPT_TOOL_ROTATE, + SCULPT_TOOL_SNAKE_HOOK, + SCULPT_TOOL_THUMB)) { + return false; + } + else if (SCULPT_is_cloth_deform_brush(brush)) { + return false; + } + else { + return true; + } + default: + break; + } + + return true; +} + +static bool paint_stroke_use_scene_spacing(Brush *brush, ePaintMode mode) +{ + switch (mode) { + case PAINT_MODE_SCULPT: + return brush->flag & BRUSH_SCENE_SPACING; + default: + break; + } + return false; +} + +static bool paint_tool_raycast_original(Brush *brush, ePaintMode UNUSED(mode)) +{ + return brush->flag & BRUSH_ANCHORED; +} + +static bool paint_tool_require_inbetween_mouse_events(Brush *brush, ePaintMode mode) +{ + if (brush->flag & BRUSH_ANCHORED) { + return false; + } + + switch (mode) { + case PAINT_MODE_SCULPT: + if (ELEM(brush->sculpt_tool, + SCULPT_TOOL_GRAB, + SCULPT_TOOL_ROTATE, + SCULPT_TOOL_THUMB, + SCULPT_TOOL_SNAKE_HOOK, + SCULPT_TOOL_ELASTIC_DEFORM, + SCULPT_TOOL_CLOTH, + SCULPT_TOOL_BOUNDARY, + SCULPT_TOOL_POSE)) { + return false; + } + else { + return true; + } + default: + break; + } + + return true; +} + +/* Initialize the stroke cache variants from operator properties */ +static bool paint_brush_update(bContext *C, + Brush *brush, + ePaintMode mode, + struct PaintStroke *stroke, + const float mouse_init[2], + float mouse[2], + float pressure, + float r_location[3], + bool *r_location_is_set) +{ + Scene *scene = CTX_data_scene(C); + UnifiedPaintSettings *ups = stroke->ups; + bool location_sampled = false; + bool location_success = false; + /* Use to perform all operations except applying the stroke, + * needed for operations that require cursor motion (rake). */ + bool is_dry_run = false; + bool do_random = false; + bool do_random_mask = false; + *r_location_is_set = false; + /* XXX: Use pressure value from first brush step for brushes which don't + * support strokes (grab, thumb). They depends on initial state and + * brush coord/pressure/etc. + * It's more an events design issue, which doesn't split coordinate/pressure/angle + * changing events. We should avoid this after events system re-design */ + if (!stroke->brush_init) { + copy_v2_v2(stroke->initial_mouse, mouse); + copy_v2_v2(ups->last_rake, mouse); + copy_v2_v2(ups->tex_mouse, mouse); + copy_v2_v2(ups->mask_tex_mouse, mouse); + stroke->cached_size_pressure = pressure; + + ups->do_linear_conversion = false; + ups->colorspace = NULL; + + /* check here if color sampling the main brush should do color conversion. This is done here + * to avoid locking up to get the image buffer during sampling */ + if (brush->mtex.tex && brush->mtex.tex->type == TEX_IMAGE && brush->mtex.tex->ima) { + ImBuf *tex_ibuf = BKE_image_pool_acquire_ibuf( + brush->mtex.tex->ima, &brush->mtex.tex->iuser, NULL); + if (tex_ibuf && tex_ibuf->rect_float == NULL) { + ups->do_linear_conversion = true; + ups->colorspace = tex_ibuf->rect_colorspace; + } + BKE_image_pool_release_ibuf(brush->mtex.tex->ima, tex_ibuf, NULL); + } + + stroke->brush_init = true; + } + + if (paint_supports_dynamic_size(brush, mode)) { + copy_v2_v2(ups->tex_mouse, mouse); + copy_v2_v2(ups->mask_tex_mouse, mouse); + stroke->cached_size_pressure = pressure; + } + + /* Truly temporary data that isn't stored in properties */ + + ups->stroke_active = true; + ups->size_pressure_value = stroke->cached_size_pressure; + + ups->pixel_radius = BKE_brush_size_get(scene, brush); + ups->initial_pixel_radius = BKE_brush_size_get(scene, brush); + + if (BKE_brush_use_size_pressure(brush) && paint_supports_dynamic_size(brush, mode)) { + ups->pixel_radius *= stroke->cached_size_pressure; + } + + if (paint_supports_dynamic_tex_coords(brush, mode)) { + + if (ELEM(brush->mtex.brush_map_mode, + MTEX_MAP_MODE_VIEW, + MTEX_MAP_MODE_AREA, + MTEX_MAP_MODE_RANDOM)) { + do_random = true; + } + + if (brush->mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) { + BKE_brush_randomize_texture_coords(ups, false); + } + else { + copy_v2_v2(ups->tex_mouse, mouse); + } + + /* take care of mask texture, if any */ + if (brush->mask_mtex.tex) { + + if (ELEM(brush->mask_mtex.brush_map_mode, + MTEX_MAP_MODE_VIEW, + MTEX_MAP_MODE_AREA, + MTEX_MAP_MODE_RANDOM)) { + do_random_mask = true; + } + + if (brush->mask_mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) { + BKE_brush_randomize_texture_coords(ups, true); + } + else { + copy_v2_v2(ups->mask_tex_mouse, mouse); + } + } + } + + if (brush->flag & BRUSH_ANCHORED) { + bool hit = false; + float halfway[2]; + + const float dx = mouse[0] - stroke->initial_mouse[0]; + const float dy = mouse[1] - stroke->initial_mouse[1]; + + ups->anchored_size = ups->pixel_radius = sqrtf(dx * dx + dy * dy); + + ups->brush_rotation = ups->brush_rotation_sec = atan2f(dx, dy) + (float)M_PI; + + if (brush->flag & BRUSH_EDGE_TO_EDGE) { + halfway[0] = dx * 0.5f + stroke->initial_mouse[0]; + halfway[1] = dy * 0.5f + stroke->initial_mouse[1]; + + if (stroke->get_location) { + if (stroke->get_location(C, r_location, halfway, stroke->original)) { + hit = true; + location_sampled = true; + location_success = true; + *r_location_is_set = true; + } + else if (!paint_tool_require_location(brush, mode)) { + hit = true; + } + } + else { + hit = true; + } + } + if (hit) { + copy_v2_v2(ups->anchored_initial_mouse, halfway); + copy_v2_v2(ups->tex_mouse, halfway); + copy_v2_v2(ups->mask_tex_mouse, halfway); + copy_v2_v2(mouse, halfway); + ups->anchored_size /= 2.0f; + ups->pixel_radius /= 2.0f; + stroke->stroke_distance = ups->pixel_radius; + } + else { + copy_v2_v2(ups->anchored_initial_mouse, stroke->initial_mouse); + copy_v2_v2(mouse, stroke->initial_mouse); + stroke->stroke_distance = ups->pixel_radius; + } + ups->pixel_radius /= stroke->zoom_2d; + ups->draw_anchored = true; + } + else { + /* here we are using the initial mouse coordinate because we do not want the rake + * result to depend on jittering */ + if (!stroke->brush_init) { + copy_v2_v2(ups->last_rake, mouse_init); + } + /* curve strokes do their own rake calculation */ + else if (!(brush->flag & BRUSH_CURVE)) { + if (!paint_calculate_rake_rotation(ups, brush, mouse_init)) { + /* Not enough motion to define an angle. */ + if (!stroke->rake_started) { + is_dry_run = true; + } + } + else { + stroke->rake_started = true; + } + } + } + + if ((do_random || do_random_mask) && stroke->rng == NULL) { + /* Lazy initialization. */ + uint rng_seed = (uint)(PIL_check_seconds_timer_i() & UINT_MAX); + rng_seed ^= (uint)POINTER_AS_INT(brush); + stroke->rng = BLI_rng_new(rng_seed); + } + + if (do_random) { + if (brush->mtex.brush_angle_mode & MTEX_ANGLE_RANDOM) { + ups->brush_rotation += -brush->mtex.random_angle / 2.0f + + brush->mtex.random_angle * BLI_rng_get_float(stroke->rng); + } + } + + if (do_random_mask) { + if (brush->mask_mtex.brush_angle_mode & MTEX_ANGLE_RANDOM) { + ups->brush_rotation_sec += -brush->mask_mtex.random_angle / 2.0f + + brush->mask_mtex.random_angle * BLI_rng_get_float(stroke->rng); + } + } + + if (!location_sampled) { + if (stroke->get_location) { + if (stroke->get_location(C, r_location, mouse, stroke->original)) { + location_success = true; + *r_location_is_set = true; + } + else if (!paint_tool_require_location(brush, mode)) { + location_success = true; + } + } + else { + zero_v3(r_location); + location_success = true; + /* don't set 'r_location_is_set', since we don't want to use the value. */ + } + } + + return location_success && (is_dry_run == false); +} + +static bool paint_stroke_use_dash(Brush *brush) +{ + /* Only these stroke modes support dash lines */ + return brush->flag & BRUSH_SPACE || brush->flag & BRUSH_LINE || brush->flag & BRUSH_CURVE; +} + +static bool paint_stroke_use_jitter(ePaintMode mode, Brush *brush, bool invert) +{ + bool use_jitter = (brush->flag & BRUSH_ABSOLUTE_JITTER) ? (brush->jitter_absolute != 0) : + (brush->jitter != 0); + + /* jitter-ed brush gives weird and unpredictable result for this + * kinds of stroke, so manually disable jitter usage (sergey) */ + use_jitter &= (brush->flag & (BRUSH_DRAG_DOT | BRUSH_ANCHORED)) == 0; + use_jitter &= (!ELEM(mode, PAINT_MODE_TEXTURE_2D, PAINT_MODE_TEXTURE_3D) || + !(invert && brush->imagepaint_tool == PAINT_TOOL_CLONE)); + + return use_jitter; +} + +/* Put the location of the next stroke dot into the stroke RNA and apply it to the mesh */ +static void paint_brush_stroke_add_step( + bContext *C, wmOperator *op, PaintStroke *stroke, const float mval[2], float pressure) +{ + Scene *scene = CTX_data_scene(C); + Paint *paint = BKE_paint_get_active_from_context(C); + ePaintMode mode = BKE_paintmode_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + UnifiedPaintSettings *ups = stroke->ups; + float mouse_out[2]; + PointerRNA itemptr; + float location[3]; + +/* the following code is adapted from texture paint. It may not be needed but leaving here + * just in case for reference (code in texpaint removed as part of refactoring). + * It's strange that only texpaint had these guards. */ +#if 0 + /* special exception here for too high pressure values on first touch in + * windows for some tablets, then we just skip first touch. */ + if (tablet && (pressure >= 0.99f) && + ((pop->s.brush->flag & BRUSH_SPACING_PRESSURE) || + BKE_brush_use_alpha_pressure(pop->s.brush) || + BKE_brush_use_size_pressure(pop->s.brush))) { + return; + } + + /* This can be removed once fixed properly in + * BKE_brush_painter_paint( + * BrushPainter *painter, BrushFunc func, + * float *pos, double time, float pressure, void *user); + * at zero pressure we should do nothing 1/2^12 is 0.0002 + * which is the sensitivity of the most sensitive pen tablet available */ + if (tablet && (pressure < 0.0002f) && + ((pop->s.brush->flag & BRUSH_SPACING_PRESSURE) || + BKE_brush_use_alpha_pressure(pop->s.brush) || + BKE_brush_use_size_pressure(pop->s.brush))) { + return; + } +#endif + + /* copy last position -before- jittering, or space fill code + * will create too many dabs */ + copy_v2_v2(stroke->last_mouse_position, mval); + stroke->last_pressure = pressure; + + if (paint_stroke_use_scene_spacing(brush, mode)) { + float world_space_position[3]; + + if (SCULPT_stroke_get_location( + C, world_space_position, stroke->last_mouse_position, stroke->original)) { + copy_v3_v3(stroke->last_world_space_position, world_space_position); + mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); + } + else { + add_v3_v3(stroke->last_world_space_position, stroke->last_scene_spacing_delta); + } + } + + if (paint_stroke_use_jitter(mode, brush, stroke->stroke_mode == BRUSH_STROKE_INVERT)) { + float delta[2]; + float factor = stroke->zoom_2d; + + if (brush->flag & BRUSH_JITTER_PRESSURE) { + factor *= pressure; + } + + BKE_brush_jitter_pos(scene, brush, mval, mouse_out); + + /* XXX: meh, this is round about because + * BKE_brush_jitter_pos isn't written in the best way to + * be reused here */ + if (factor != 1.0f) { + sub_v2_v2v2(delta, mouse_out, mval); + mul_v2_fl(delta, factor); + add_v2_v2v2(mouse_out, mval, delta); + } + } + else { + copy_v2_v2(mouse_out, mval); + } + + bool is_location_is_set; + ups->last_hit = paint_brush_update( + C, brush, mode, stroke, mval, mouse_out, pressure, location, &is_location_is_set); + if (is_location_is_set) { + copy_v3_v3(ups->last_location, location); + } + if (!ups->last_hit) { + return; + } + + /* Dash */ + bool add_step = true; + if (paint_stroke_use_dash(brush)) { + int dash_samples = stroke->tot_samples % brush->dash_samples; + float dash = (float)dash_samples / (float)brush->dash_samples; + if (dash > brush->dash_ratio) { + add_step = false; + } + } + + /* Add to stroke */ + if (add_step) { + PaintStrokePoint *point; + PaintStrokePoint temp; + + int n = 1; + int max_points = paint_stroke_max_points(paint, stroke); + + if (stroke->num_points < max_points) { + // n = max_points - stroke->num_points; + } + for (int i = 0; i < n; i++) { + paint_stroke_add_point(paint, + stroke, + mval, + mouse_out, + location, + ups->pixel_radius, + pressure, + stroke->pen_flip, + stroke->x_tilt, + stroke->y_tilt); + if (stroke->need_roll_mapping) { + paint_brush_make_spline(C, stroke); + } + } + + if (stroke->need_roll_mapping) { + if (stroke->spline->segments.size() < paint_stroke_max_points(paint, stroke)) { + return; + } + + int cur = stroke->cur_point - (paint_stroke_max_points(paint, stroke) >> 1) - 2; + cur = (cur + stroke->num_points) % stroke->num_points; + + PaintStrokePoint *p1 = stroke->points + ((cur + stroke->num_points) % stroke->num_points); + PaintStrokePoint *p2 = stroke->points + + ((cur - 1 + stroke->num_points) % stroke->num_points); + + point = &temp; + temp = *p1; + + interp_v3_v3v3(temp.location, p1->location, p2->location, 0.5f); + interp_v2_v2v2(temp.mouse_in, p1->mouse_in, p2->mouse_in, 0.5f); + interp_v2_v2v2(temp.mouse_out, p1->mouse_out, p2->mouse_out, 0.5f); + } + else { + point = stroke->points + ((stroke->cur_point - 1 + stroke->num_points) % stroke->num_points); + } + + RNA_collection_add(op->ptr, "stroke", &itemptr); + RNA_float_set(&itemptr, "size", point->size); + RNA_float_set_array(&itemptr, "location", point->location); + /* Mouse coordinates modified by the stroke type options. */ + RNA_float_set_array(&itemptr, "mouse", point->mouse_out); + /* Original mouse coordinates. */ + RNA_float_set_array(&itemptr, "mouse_event", point->mouse_in); + RNA_boolean_set(&itemptr, "pen_flip", point->pen_flip); + RNA_float_set(&itemptr, "pressure", point->pressure); + RNA_float_set(&itemptr, "x_tilt", point->x_tilt); + RNA_float_set(&itemptr, "y_tilt", point->y_tilt); + + stroke->update_step(C, op, stroke, &itemptr); + + /* don't record this for now, it takes up a lot of memory when doing long + * strokes with small brush size, and operators have register disabled */ + RNA_collection_clear(op->ptr, "stroke"); + } + + stroke->tot_samples++; +} + +/* Returns zero if no sculpt changes should be made, non-zero otherwise */ +static bool paint_smooth_stroke(PaintStroke *stroke, + const PaintSample *sample, + ePaintMode mode, + float r_mouse[2], + float *r_pressure) +{ + if (paint_supports_smooth_stroke(stroke->brush, mode)) { + float radius = stroke->brush->smooth_stroke_radius * stroke->zoom_2d; + float u = stroke->brush->smooth_stroke_factor; + + /* If the mouse is moving within the radius of the last move, + * don't update the mouse position. This allows sharp turns. */ + if (len_squared_v2v2(stroke->last_mouse_position, sample->mouse) < square_f(radius)) { + return false; + } + + interp_v2_v2v2(r_mouse, sample->mouse, stroke->last_mouse_position, u); + *r_pressure = interpf(sample->pressure, stroke->last_pressure, u); + } + else { + r_mouse[0] = sample->mouse[0]; + r_mouse[1] = sample->mouse[1]; + *r_pressure = sample->pressure; + } + + return true; +} + +static float paint_space_stroke_spacing(bContext *C, + const Scene *scene, + PaintStroke *stroke, + float size_pressure, + float spacing_pressure) +{ + Paint *paint = BKE_paint_get_active_from_context(C); + ePaintMode mode = BKE_paintmode_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + float size_clamp = 0.0f; + float size = BKE_brush_size_get(scene, stroke->brush) * size_pressure; + if (paint_stroke_use_scene_spacing(brush, mode)) { + if (!BKE_brush_use_locked_size(scene, brush)) { + float last_object_space_position[3]; + mul_v3_m4v3(last_object_space_position, + stroke->vc.obact->world_to_object, + stroke->last_world_space_position); + size_clamp = paint_calc_object_space_radius(&stroke->vc, last_object_space_position, size); + } + else { + size_clamp = BKE_brush_unprojected_radius_get(scene, brush) * size_pressure; + } + } + else { + /* brushes can have a minimum size of 1.0 but with pressure it can be smaller than a pixel + * causing very high step sizes, hanging blender T32381. */ + size_clamp = max_ff(1.0f, size); + } + + float spacing = stroke->brush->spacing; + + /* apply spacing pressure */ + if (stroke->brush->flag & BRUSH_SPACING_PRESSURE) { + spacing = spacing * (1.5f - spacing_pressure); + } + + stroke->spacing_raw = spacing * 0.01; + + if (SCULPT_is_cloth_deform_brush(brush)) { + /* The spacing in tools that use the cloth solver should not be affected by the brush radius to + * avoid affecting the simulation update rate when changing the radius of the brush. + * With a value of 100 and the brush default of 10 for spacing, a simulation step runs every 2 + * pixels movement of the cursor. */ + size_clamp = 100.0f; + } + + /* stroke system is used for 2d paint too, so we need to account for + * the fact that brush can be scaled there. */ + spacing *= stroke->zoom_2d; + + if (paint_stroke_use_scene_spacing(brush, mode)) { + return size_clamp * spacing / 50.0f; + } + return max_ff(stroke->zoom_2d, size_clamp * spacing / 50.0f); +} + +static float paint_stroke_overlapped_curve(Brush *br, float x, float spacing) +{ + const int n = 100 / spacing; + const float h = spacing / 50.0f; + const float x0 = x - 1; + + float sum = 0; + for (int i = 0; i < n; i++) { + float xx; + + xx = fabsf(x0 + i * h); + + if (xx < 1.0f) { + sum += BKE_brush_curve_strength(br, xx, 1); + } + } + + return sum; +} + +static float paint_stroke_integrate_overlap(Brush *br, float factor) +{ + float spacing = br->spacing * factor; + + if (!(br->flag & BRUSH_SPACE_ATTEN && (br->spacing < 100))) { + return 1.0; + } + + int m = 10; + float g = 1.0f / m; + float max = 0; + for (int i = 0; i < m; i++) { + float overlap = fabs(paint_stroke_overlapped_curve(br, i * g, spacing)); + + if (overlap > max) { + max = overlap; + } + } + + if (max == 0.0f) { + return 1.0f; + } + return 1.0f / max; +} + +static float paint_space_stroke_spacing_variable(bContext *C, + const Scene *scene, + PaintStroke *stroke, + float pressure, + float dpressure, + float length) +{ + if (BKE_brush_use_size_pressure(stroke->brush)) { + /* use pressure to modify size. set spacing so that at 100%, the circles + * are aligned nicely with no overlap. for this the spacing needs to be + * the average of the previous and next size. */ + float s = paint_space_stroke_spacing(C, scene, stroke, 1.0f, pressure); + float q = s * dpressure / (2.0f * length); + float pressure_fac = (1.0f + q) / (1.0f - q); + + float last_size_pressure = stroke->last_pressure; + float new_size_pressure = stroke->last_pressure * pressure_fac; + + /* average spacing */ + float last_spacing = paint_space_stroke_spacing( + C, scene, stroke, last_size_pressure, pressure); + float new_spacing = paint_space_stroke_spacing(C, scene, stroke, new_size_pressure, pressure); + + return 0.5f * (last_spacing + new_spacing); + } + + /* no size pressure */ + return paint_space_stroke_spacing(C, scene, stroke, 1.0f, pressure); +} + +/* For brushes with stroke spacing enabled, moves mouse in steps + * towards the final mouse location. */ +static int paint_space_stroke(bContext *C, + wmOperator *op, + PaintStroke *stroke, + const float final_mouse[2], + float final_pressure) +{ + const Scene *scene = CTX_data_scene(C); + ARegion *region = CTX_wm_region(C); + UnifiedPaintSettings *ups = stroke->ups; + Paint *paint = BKE_paint_get_active_from_context(C); + ePaintMode mode = BKE_paintmode_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + int count = 0; + + const bool use_scene_spacing = paint_stroke_use_scene_spacing(brush, mode); + float d_world_space_position[3] = {0.0f}; + + float no_pressure_spacing = paint_space_stroke_spacing(C, scene, stroke, 1.0f, 1.0f); + float pressure = stroke->last_pressure; + float dpressure = final_pressure - stroke->last_pressure; + + float dmouse[2]; + sub_v2_v2v2(dmouse, final_mouse, stroke->last_mouse_position); + float length = normalize_v2(dmouse); + + if (use_scene_spacing) { + float world_space_position[3]; + bool hit = SCULPT_stroke_get_location(C, world_space_position, final_mouse, stroke->original); + mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position); + if (hit && stroke->stroke_over_mesh) { + sub_v3_v3v3(d_world_space_position, world_space_position, stroke->last_world_space_position); + length = len_v3(d_world_space_position); + stroke->stroke_over_mesh = true; + } + else { + length = 0.0f; + zero_v3(d_world_space_position); + stroke->stroke_over_mesh = hit; + if (stroke->stroke_over_mesh) { + copy_v3_v3(stroke->last_world_space_position, world_space_position); + } + } + } + + while (length > 0.0f) { + float spacing = paint_space_stroke_spacing_variable( + C, scene, stroke, pressure, dpressure, length); + float mouse[2]; + + if (length >= spacing) { + if (use_scene_spacing) { + float final_world_space_position[3]; + normalize_v3(d_world_space_position); + mul_v3_v3fl(final_world_space_position, d_world_space_position, spacing); + add_v3_v3v3(final_world_space_position, + stroke->last_world_space_position, + final_world_space_position); + ED_view3d_project_v2(region, final_world_space_position, mouse); + + mul_v3_v3fl(stroke->last_scene_spacing_delta, d_world_space_position, spacing); + } + else { + mouse[0] = stroke->last_mouse_position[0] + dmouse[0] * spacing; + mouse[1] = stroke->last_mouse_position[1] + dmouse[1] * spacing; + } + pressure = stroke->last_pressure + (spacing / length) * dpressure; + + ups->overlap_factor = paint_stroke_integrate_overlap(stroke->brush, + spacing / no_pressure_spacing); + + stroke->stroke_distance += spacing / stroke->zoom_2d; + paint_brush_stroke_add_step(C, op, stroke, mouse, pressure); + + length -= spacing; + pressure = stroke->last_pressure; + dpressure = final_pressure - stroke->last_pressure; + + count++; + } + else { + break; + } + } + + return count; +} + +/**** Public API ****/ + +PaintStroke *paint_stroke_new(bContext *C, + wmOperator *op, + StrokeGetLocation get_location, + StrokeTestStart test_start, + StrokeUpdateStep update_step, + StrokeRedraw redraw, + StrokeDone done, + int event_type) +{ + struct Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + PaintStroke *stroke = (PaintStroke *)MEM_callocN(sizeof(PaintStroke), "PaintStroke"); + ToolSettings *toolsettings = CTX_data_tool_settings(C); + UnifiedPaintSettings *ups = &toolsettings->unified_paint_settings; + Paint *p = BKE_paint_get_active_from_context(C); + Brush *br = stroke->brush = BKE_paint_brush(p); + RegionView3D *rv3d = CTX_wm_region_view3d(C); + float zoomx, zoomy; + + ED_view3d_viewcontext_init(C, &stroke->vc, depsgraph); + +#ifdef DRAW_DEBUG_VIS + ARegion *region = CTX_wm_region(C); + + stroke->debug_draw_handle = ED_region_draw_cb_activate( + region->type, paint_brush_cubic_vis, stroke, REGION_DRAW_POST_VIEW); +#endif + + stroke->get_location = get_location; + stroke->test_start = test_start; + stroke->update_step = update_step; + stroke->redraw = redraw; + stroke->done = done; + stroke->event_type = event_type; /* for modal, return event */ + stroke->ups = ups; + stroke->stroke_mode = RNA_enum_get(op->ptr, "mode"); + + stroke->original = paint_tool_raycast_original(br, BKE_paintmode_get_active_from_context(C)); + + get_imapaint_zoom(C, &zoomx, &zoomy); + stroke->zoom_2d = max_ff(zoomx, zoomy); + + if (br->mtex.tex && br->mtex.brush_map_mode == MTEX_MAP_MODE_ROLL) { + stroke->need_roll_mapping = true; + } + if (br->mask_mtex.tex && br->mask_mtex.brush_map_mode == MTEX_MAP_MODE_ROLL) { + stroke->need_roll_mapping = true; + } + + if (stroke->need_roll_mapping) { + stroke->spline = MEM_new("BezierSpline2f"); + stroke->world_spline = MEM_new("BezierSpline3f"); + } + + if (stroke->stroke_mode == BRUSH_STROKE_INVERT) { + if (br->flag & BRUSH_CURVE) { + RNA_enum_set(op->ptr, "mode", BRUSH_STROKE_NORMAL); + } + } + /* initialize here */ + ups->overlap_factor = 1.0; + ups->stroke_active = true; + + if (rv3d) { + rv3d->rflag |= RV3D_PAINTING; + } + + /* Preserve location from last stroke while applying and resetting + * ups->average_stroke_counter to 1. + */ + if (ups->average_stroke_counter) { + mul_v3_fl(ups->average_stroke_accum, 1.0f / (float)ups->average_stroke_counter); + ups->average_stroke_counter = 1; + } + + /* initialize here to avoid initialization conflict with threaded strokes */ + BKE_curvemapping_init(br->curve); + if (p->flags & PAINT_USE_CAVITY_MASK) { + BKE_curvemapping_init(p->cavity_curve); + } + + BKE_paint_set_overlay_override((eOverlayFlags)br->overlay_flags); + + ups->start_pixel_radius = BKE_brush_size_get(CTX_data_scene(C), br); + + return stroke; +} + +void paint_stroke_free(bContext *C, wmOperator *UNUSED(op), PaintStroke *stroke) +{ + RegionView3D *rv3d = CTX_wm_region_view3d(C); + if (rv3d) { + rv3d->rflag &= ~RV3D_PAINTING; + } + + BKE_paint_set_overlay_override((eOverlayFlags)0); + + if (stroke == NULL) { + return; + } + + UnifiedPaintSettings *ups = stroke->ups; + ups->draw_anchored = false; + ups->stroke_active = false; + + if (stroke->timer) { + WM_event_remove_timer(CTX_wm_manager(C), CTX_wm_window(C), stroke->timer); + } + + if (stroke->rng) { + BLI_rng_free(stroke->rng); + } + + if (stroke->stroke_cursor) { + WM_paint_cursor_end((wmPaintCursor *)stroke->stroke_cursor); + } + + BLI_freelistN(&stroke->line); + +#ifdef DRAW_DEBUG_VIS + ARegion *region = CTX_wm_region(C); + + ED_region_draw_cb_exit(region->type, stroke->debug_draw_handle); + ED_region_tag_redraw(region); +#endif + + MEM_SAFE_FREE(stroke); +} + +static void stroke_done(bContext *C, wmOperator *op, PaintStroke *stroke) +{ + UnifiedPaintSettings *ups = stroke->ups; + + /* reset rotation here to avoid doing so in cursor display */ + if (!(stroke->brush->mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { + ups->brush_rotation = 0.0f; + } + + if (!(stroke->brush->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { + ups->brush_rotation_sec = 0.0f; + } + + if (stroke->stroke_started) { + if (stroke->redraw) { + stroke->redraw(C, stroke, true); + } + + if (stroke->done) { + stroke->done(C, stroke); + } + } + + paint_stroke_free(C, op, stroke); +} + +static bool curves_sculpt_brush_uses_spacing(const eBrushCurvesSculptTool tool) +{ + return ELEM(tool, CURVES_SCULPT_TOOL_ADD, CURVES_SCULPT_TOOL_DENSITY); +} + +bool paint_space_stroke_enabled(Brush *br, ePaintMode mode) +{ + if ((br->flag & BRUSH_SPACE) == 0) { + return false; + } + + if (br->sculpt_tool == SCULPT_TOOL_CLOTH || SCULPT_is_cloth_deform_brush(br)) { + /* The Cloth Brush is a special case for stroke spacing. Even if it has grab modes which do + * not support dynamic size, stroke spacing needs to be enabled so it is possible to control + * whether the simulation runs constantly or only when the brush moves when using the cloth + * grab brushes. */ + return true; + } + + if (mode == PAINT_MODE_SCULPT_CURVES && + !curves_sculpt_brush_uses_spacing((eBrushCurvesSculptTool)br->curves_sculpt_tool)) { + return false; + } + + return paint_supports_dynamic_size(br, mode); +} + +static bool sculpt_is_grab_tool(Brush *br) +{ + + if (br->sculpt_tool == SCULPT_TOOL_CLOTH && br->cloth_deform_type == BRUSH_CLOTH_DEFORM_GRAB) { + return true; + } + return ELEM(br->sculpt_tool, + SCULPT_TOOL_GRAB, + SCULPT_TOOL_ELASTIC_DEFORM, + SCULPT_TOOL_POSE, + SCULPT_TOOL_BOUNDARY, + SCULPT_TOOL_THUMB, + SCULPT_TOOL_ROTATE, + SCULPT_TOOL_SNAKE_HOOK); +} + +bool paint_supports_dynamic_size(Brush *br, ePaintMode mode) +{ + if (br->flag & BRUSH_ANCHORED) { + return false; + } + + switch (mode) { + case PAINT_MODE_SCULPT: + if (sculpt_is_grab_tool(br)) { + return false; + } + break; + + case PAINT_MODE_TEXTURE_2D: /* fall through */ + case PAINT_MODE_TEXTURE_3D: + if ((br->imagepaint_tool == PAINT_TOOL_FILL) && (br->flag & BRUSH_USE_GRADIENT)) { + return false; + } + break; + + default: + break; + } + return true; +} + +bool paint_supports_smooth_stroke(Brush *br, ePaintMode mode) +{ + if (!(br->flag & BRUSH_SMOOTH_STROKE) || + (br->flag & (BRUSH_ANCHORED | BRUSH_DRAG_DOT | BRUSH_LINE))) { + return false; + } + + switch (mode) { + case PAINT_MODE_SCULPT: + if (sculpt_is_grab_tool(br)) { + return false; + } + break; + default: + break; + } + return true; +} + +bool paint_supports_texture(ePaintMode mode) +{ + /* omit: PAINT_WEIGHT, PAINT_SCULPT_UV, PAINT_INVALID */ + return ELEM( + mode, PAINT_MODE_SCULPT, PAINT_MODE_VERTEX, PAINT_MODE_TEXTURE_3D, PAINT_MODE_TEXTURE_2D); +} + +bool paint_supports_dynamic_tex_coords(Brush *br, ePaintMode mode) +{ + if (br->flag & BRUSH_ANCHORED) { + return false; + } + + switch (mode) { + case PAINT_MODE_SCULPT: + if (sculpt_is_grab_tool(br)) { + return false; + } + break; + default: + break; + } + return true; +} + +#define PAINT_STROKE_MODAL_CANCEL 1 + +struct wmKeyMap *paint_stroke_modal_keymap(struct wmKeyConfig *keyconf) +{ + static struct EnumPropertyItem modal_items[] = { + {PAINT_STROKE_MODAL_CANCEL, "CANCEL", 0, "Cancel", "Cancel and undo a stroke in progress"}, + + {0}}; + + static const char *name = "Paint Stroke Modal"; + + struct wmKeyMap *keymap = WM_modalkeymap_find(keyconf, name); + + /* This function is called for each space-type, only needs to add map once. */ + if (!keymap) { + keymap = WM_modalkeymap_ensure(keyconf, name, modal_items); + } + + return keymap; +} + +static void paint_stroke_add_sample( + const Paint *paint, PaintStroke *stroke, float x, float y, float pressure) +{ + PaintSample *sample = &stroke->samples[stroke->cur_sample]; + int max_samples = CLAMPIS(paint->num_input_samples, 1, PAINT_MAX_INPUT_SAMPLES); + + sample->mouse[0] = x; + sample->mouse[1] = y; + sample->pressure = pressure; + + stroke->cur_sample++; + if (stroke->cur_sample >= max_samples) { + stroke->cur_sample = 0; + } + if (stroke->num_samples < max_samples) { + stroke->num_samples++; + } +} + +static void paint_stroke_sample_average(const PaintStroke *stroke, PaintSample *average) +{ + memset(average, 0, sizeof(*average)); + + BLI_assert(stroke->num_samples > 0); + + for (int i = 0; i < stroke->num_samples; i++) { + add_v2_v2(average->mouse, stroke->samples[i].mouse); + average->pressure += stroke->samples[i].pressure; + } + + mul_v2_fl(average->mouse, 1.0f / stroke->num_samples); + average->pressure /= stroke->num_samples; + + // printf("avg=(%f, %f), num=%d\n", average->mouse[0], average->mouse[1], stroke->num_samples); +} + +/** + * Slightly different version of spacing for line/curve strokes, + * makes sure the dabs stay on the line path. + */ +static void paint_line_strokes_spacing(bContext *C, + wmOperator *op, + PaintStroke *stroke, + float spacing, + float *length_residue, + const float old_pos[2], + const float new_pos[2]) +{ + UnifiedPaintSettings *ups = stroke->ups; + Paint *paint = BKE_paint_get_active_from_context(C); + Brush *brush = BKE_paint_brush(paint); + ePaintMode mode = BKE_paintmode_get_active_from_context(C); + ARegion *region = CTX_wm_region(C); + + const bool use_scene_spacing = paint_stroke_use_scene_spacing(brush, mode); + + float mouse[2], dmouse[2]; + float length; + float d_world_space_position[3] = {0.0f}; + float world_space_position_old[3], world_space_position_new[3]; + + copy_v2_v2(stroke->last_mouse_position, old_pos); + + if (use_scene_spacing) { + bool hit_old = SCULPT_stroke_get_location( + C, world_space_position_old, old_pos, stroke->original); + bool hit_new = SCULPT_stroke_get_location( + C, world_space_position_new, new_pos, stroke->original); + mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position_old); + mul_m4_v3(stroke->vc.obact->object_to_world, world_space_position_new); + if (hit_old && hit_new && stroke->stroke_over_mesh) { + sub_v3_v3v3(d_world_space_position, world_space_position_new, world_space_position_old); + length = len_v3(d_world_space_position); + stroke->stroke_over_mesh = true; + } + else { + length = 0.0f; + zero_v3(d_world_space_position); + stroke->stroke_over_mesh = hit_new; + if (stroke->stroke_over_mesh) { + copy_v3_v3(stroke->last_world_space_position, world_space_position_old); + } + } + } + else { + sub_v2_v2v2(dmouse, new_pos, old_pos); + length = normalize_v2(dmouse); + } + + BLI_assert(length >= 0.0f); + + if (length == 0.0f) { + return; + } + + while (length > 0.0f) { + float spacing_final = spacing - *length_residue; + length += *length_residue; + *length_residue = 0.0; + + if (length >= spacing) { + if (use_scene_spacing) { + float final_world_space_position[3]; + normalize_v3(d_world_space_position); + mul_v3_v3fl(final_world_space_position, d_world_space_position, spacing_final); + add_v3_v3v3( + final_world_space_position, world_space_position_old, final_world_space_position); + ED_view3d_project_v2(region, final_world_space_position, mouse); + } + else { + mouse[0] = stroke->last_mouse_position[0] + dmouse[0] * spacing_final; + mouse[1] = stroke->last_mouse_position[1] + dmouse[1] * spacing_final; + } + + ups->overlap_factor = paint_stroke_integrate_overlap(stroke->brush, 1.0); + + stroke->stroke_distance += spacing / stroke->zoom_2d; + paint_brush_stroke_add_step(C, op, stroke, mouse, 1.0); + + length -= spacing; + spacing_final = spacing; + } + else { + break; + } + } + + *length_residue = length; +} + +static void paint_stroke_line_end(bContext *C, + wmOperator *op, + PaintStroke *stroke, + const float mouse[2]) +{ + Brush *br = stroke->brush; + if (stroke->stroke_started && (br->flag & BRUSH_LINE)) { + stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); + + paint_brush_stroke_add_step(C, op, stroke, stroke->last_mouse_position, 1.0); + paint_space_stroke(C, op, stroke, mouse, 1.0); + } +} + +static bool paint_stroke_curve_end(bContext *C, wmOperator *op, PaintStroke *stroke) +{ + Brush *br = stroke->brush; + + if (br->flag & BRUSH_CURVE) { + UnifiedPaintSettings *ups = &CTX_data_tool_settings(C)->unified_paint_settings; + const Scene *scene = CTX_data_scene(C); + const float spacing = paint_space_stroke_spacing(C, scene, stroke, 1.0f, 1.0f); + PaintCurve *pc = br->paint_curve; + PaintCurvePoint *pcp; + float length_residue = 0.0f; + int i; + + if (!pc) { + return true; + } + +#ifdef DEBUG_TIME + TIMEIT_START_AVERAGED(whole_stroke); +#endif + + pcp = pc->points; + stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); + + for (i = 0; i < pc->tot_points - 1; i++, pcp++) { + int j; + float data[(PAINT_CURVE_NUM_SEGMENTS + 1) * 2]; + float tangents[(PAINT_CURVE_NUM_SEGMENTS + 1) * 2]; + PaintCurvePoint *pcp_next = pcp + 1; + bool do_rake = false; + + for (j = 0; j < 2; j++) { + BKE_curve_forward_diff_bezier(pcp->bez.vec[1][j], + pcp->bez.vec[2][j], + pcp_next->bez.vec[0][j], + pcp_next->bez.vec[1][j], + data + j, + PAINT_CURVE_NUM_SEGMENTS, + sizeof(float[2])); + } + + if ((br->mtex.brush_angle_mode & MTEX_ANGLE_RAKE) || + (br->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { + do_rake = true; + for (j = 0; j < 2; j++) { + BKE_curve_forward_diff_tangent_bezier(pcp->bez.vec[1][j], + pcp->bez.vec[2][j], + pcp_next->bez.vec[0][j], + pcp_next->bez.vec[1][j], + tangents + j, + PAINT_CURVE_NUM_SEGMENTS, + sizeof(float[2])); + } + } + + for (j = 0; j < PAINT_CURVE_NUM_SEGMENTS; j++) { + if (do_rake) { + float rotation = atan2f(tangents[2 * j], tangents[2 * j + 1]); + paint_update_brush_rake_rotation(ups, br, rotation); + } + + if (!stroke->stroke_started) { + stroke->last_pressure = 1.0; + copy_v2_v2(stroke->last_mouse_position, data + 2 * j); + + if (paint_stroke_use_scene_spacing(br, BKE_paintmode_get_active_from_context(C))) { + stroke->stroke_over_mesh = SCULPT_stroke_get_location( + C, stroke->last_world_space_position, data + 2 * j, stroke->original); + mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); + } + + stroke->stroke_started = stroke->test_start(C, op, stroke->last_mouse_position); + + if (stroke->stroke_started) { + paint_brush_stroke_add_step(C, op, stroke, data + 2 * j, 1.0); + paint_line_strokes_spacing( + C, op, stroke, spacing, &length_residue, data + 2 * j, data + 2 * (j + 1)); + } + } + else { + paint_line_strokes_spacing( + C, op, stroke, spacing, &length_residue, data + 2 * j, data + 2 * (j + 1)); + } + } + } + + stroke_done(C, op, stroke); + +#ifdef DEBUG_TIME + TIMEIT_END_AVERAGED(whole_stroke); +#endif + + return true; + } + + return false; +} + +static void paint_stroke_line_constrain(PaintStroke *stroke, float mouse[2]) +{ + if (stroke->constrain_line) { + float line[2]; + float angle, len, res; + + sub_v2_v2v2(line, mouse, stroke->last_mouse_position); + angle = atan2f(line[1], line[0]); + len = len_v2(line); + + /* divide angle by PI/4 */ + angle = 4.0f * angle / (float)M_PI; + + /* now take residue */ + res = angle - floorf(angle); + + /* residue decides how close we are at a certain angle */ + if (res <= 0.5f) { + angle = floorf(angle) * (float)M_PI_4; + } + else { + angle = (floorf(angle) + 1.0f) * (float)M_PI_4; + } + + mouse[0] = stroke->constrained_pos[0] = len * cosf(angle) + stroke->last_mouse_position[0]; + mouse[1] = stroke->constrained_pos[1] = len * sinf(angle) + stroke->last_mouse_position[1]; + } +} + +int paint_stroke_modal(bContext *C, wmOperator *op, const wmEvent *event, PaintStroke **stroke_p) +{ + Paint *p = BKE_paint_get_active_from_context(C); + ePaintMode mode = BKE_paintmode_get_active_from_context(C); + PaintStroke *stroke = *stroke_p; + Brush *br = stroke->brush = BKE_paint_brush(p); + PaintSample sample_average; + float mouse[2]; + bool first_dab = false; + bool first_modal = false; + bool redraw = false; + float pressure; + + if (event->type == INBETWEEN_MOUSEMOVE && !paint_tool_require_inbetween_mouse_events(br, mode)) { + return OPERATOR_RUNNING_MODAL; + } + + /* see if tablet affects event. Line, anchored and drag dot strokes do not support pressure */ + pressure = ((br->flag & (BRUSH_LINE | BRUSH_ANCHORED | BRUSH_DRAG_DOT)) ? + 1.0f : + WM_event_tablet_data(event, &stroke->pen_flip, NULL)); + + /* When processing a timer event the pressure from the event is 0, so use the last valid + * pressure. */ + if (event->type == TIMER) { + pressure = stroke->last_tablet_event_pressure; + } + else { + stroke->last_tablet_event_pressure = pressure; + } + + if (!stroke->need_roll_mapping) { + paint_stroke_add_sample(p, stroke, event->mval[0], event->mval[1], pressure); + paint_stroke_sample_average(stroke, &sample_average); + } + else { + sample_average.mouse[0] = (float)event->mval[0]; + sample_average.mouse[1] = (float)event->mval[1]; + sample_average.pressure = pressure; + } + + if (stroke->stroke_sample_index == 0) { + stroke->last_mouse_position[0] = event->mval[0]; + stroke->last_mouse_position[1] = event->mval[1]; + } + + stroke->stroke_sample_index++; + + /* Tilt. */ + if (WM_event_is_tablet(event)) { + stroke->x_tilt = event->tablet.x_tilt; + stroke->y_tilt = event->tablet.y_tilt; + } + +#ifdef WITH_INPUT_NDOF + /* let NDOF motion pass through to the 3D view so we can paint and rotate simultaneously! + * this isn't perfect... even when an extra MOUSEMOVE is spoofed, the stroke discards it + * since the 2D deltas are zero -- code in this file needs to be updated to use the + * post-NDOF_MOTION MOUSEMOVE */ + if (event->type == NDOF_MOTION) { + return OPERATOR_PASS_THROUGH; + } +#endif + + /* one time initialization */ + if (!stroke->stroke_init) { + if (paint_stroke_curve_end(C, op, stroke)) { + *stroke_p = NULL; + return OPERATOR_FINISHED; + } + + if (paint_supports_smooth_stroke(br, mode)) { + stroke->stroke_cursor = WM_paint_cursor_activate( + SPACE_TYPE_ANY, RGN_TYPE_ANY, PAINT_brush_tool_poll, paint_draw_smooth_cursor, stroke); + } + + stroke->stroke_init = true; + first_modal = true; + } + + /* one time stroke initialization */ + if (!stroke->stroke_started) { + stroke->last_pressure = sample_average.pressure; + copy_v2_v2(stroke->last_mouse_position, sample_average.mouse); + if (paint_stroke_use_scene_spacing(br, mode)) { + stroke->stroke_over_mesh = SCULPT_stroke_get_location( + C, stroke->last_world_space_position, sample_average.mouse, stroke->original); + mul_m4_v3(stroke->vc.obact->object_to_world, stroke->last_world_space_position); + } + stroke->stroke_started = stroke->test_start(C, op, sample_average.mouse); + BLI_assert((stroke->stroke_started & ~1) == 0); /* 0/1 */ + + if (stroke->stroke_started) { + if (br->flag & BRUSH_AIRBRUSH) { + stroke->timer = WM_event_add_timer( + CTX_wm_manager(C), CTX_wm_window(C), TIMER, stroke->brush->rate); + } + + if (br->flag & BRUSH_LINE) { + stroke->stroke_cursor = WM_paint_cursor_activate( + SPACE_TYPE_ANY, RGN_TYPE_ANY, PAINT_brush_tool_poll, paint_draw_line_cursor, stroke); + } + + first_dab = true; + } + } + + /* Cancel */ + if (event->type == EVT_MODAL_MAP && event->val == PAINT_STROKE_MODAL_CANCEL) { + if (op->type->cancel) { + op->type->cancel(C, op); + } + else { + paint_stroke_cancel(C, op, stroke); + } + return OPERATOR_CANCELLED; + } + + if (event->type == stroke->event_type && !first_modal) { + if (event->val == KM_RELEASE) { + copy_v2_fl2(mouse, event->mval[0], event->mval[1]); + paint_stroke_line_constrain(stroke, mouse); + paint_stroke_line_end(C, op, stroke, mouse); + stroke_done(C, op, stroke); + *stroke_p = NULL; + return OPERATOR_FINISHED; + } + } + else if (ELEM(event->type, EVT_RETKEY, EVT_SPACEKEY)) { + paint_stroke_line_end(C, op, stroke, sample_average.mouse); + stroke_done(C, op, stroke); + *stroke_p = NULL; + return OPERATOR_FINISHED; + } + else if (br->flag & BRUSH_LINE) { + if (event->modifier & KM_ALT) { + stroke->constrain_line = true; + } + else { + stroke->constrain_line = false; + } + + copy_v2_fl2(mouse, event->mval[0], event->mval[1]); + paint_stroke_line_constrain(stroke, mouse); + + if (stroke->stroke_started && (first_modal || ISMOUSE_MOTION(event->type))) { + if ((br->mtex.brush_angle_mode & MTEX_ANGLE_RAKE) || + (br->mask_mtex.brush_angle_mode & MTEX_ANGLE_RAKE)) { + copy_v2_v2(stroke->ups->last_rake, stroke->last_mouse_position); + } + paint_calculate_rake_rotation(stroke->ups, br, mouse); + } + } + else if (first_modal || + /* regular dabs */ + (!(br->flag & BRUSH_AIRBRUSH) && ISMOUSE_MOTION(event->type)) || + /* airbrush */ + ((br->flag & BRUSH_AIRBRUSH) && event->type == TIMER && + event->customdata == stroke->timer)) { + if (paint_smooth_stroke(stroke, &sample_average, mode, mouse, &pressure)) { + if (stroke->stroke_started) { + if (paint_space_stroke_enabled(br, mode)) { + if (paint_space_stroke(C, op, stroke, mouse, pressure)) { + redraw = true; + } + } + else { + float dmouse[2]; + sub_v2_v2v2(dmouse, mouse, stroke->last_mouse_position); + stroke->stroke_distance += len_v2(dmouse); + paint_brush_stroke_add_step(C, op, stroke, mouse, pressure); + redraw = true; + } + } + } + } + + /* we want the stroke to have the first daub at the start location + * instead of waiting till we have moved the space distance */ + if (first_dab && paint_space_stroke_enabled(br, mode) && !(br->flag & BRUSH_SMOOTH_STROKE)) { + stroke->ups->overlap_factor = paint_stroke_integrate_overlap(br, 1.0); + paint_brush_stroke_add_step(C, op, stroke, sample_average.mouse, sample_average.pressure); + redraw = true; + } + + /* do updates for redraw. if event is in between mouse-move there are more + * coming, so postpone potentially slow redraw updates until all are done */ + if (event->type != INBETWEEN_MOUSEMOVE) { + wmWindow *window = CTX_wm_window(C); + ARegion *region = CTX_wm_region(C); + + /* At the very least, invalidate the cursor */ + if (region && (p->flags & PAINT_SHOW_BRUSH)) { + WM_paint_cursor_tag_redraw(window, region); + } + + if (redraw && stroke->redraw) { + stroke->redraw(C, stroke, false); + } + } + + return OPERATOR_RUNNING_MODAL; +} + +int paint_stroke_exec(bContext *C, wmOperator *op, PaintStroke *stroke) +{ + /* only when executed for the first time */ + if (stroke->stroke_started == 0) { + PropertyRNA *strokeprop; + PointerRNA firstpoint; + float mouse[2]; + + strokeprop = RNA_struct_find_property(op->ptr, "stroke"); + + if (RNA_property_collection_lookup_int(op->ptr, strokeprop, 0, &firstpoint)) { + RNA_float_get_array(&firstpoint, "mouse", mouse); + stroke->stroke_started = stroke->test_start(C, op, mouse); + } + } + + if (stroke->stroke_started) { + RNA_BEGIN (op->ptr, itemptr, "stroke") { + stroke->update_step(C, op, stroke, &itemptr); + } + RNA_END; + } + + bool ok = (stroke->stroke_started != 0); + + stroke_done(C, op, stroke); + + return ok ? OPERATOR_FINISHED : OPERATOR_CANCELLED; +} + +void paint_stroke_cancel(bContext *C, wmOperator *op, PaintStroke *stroke) +{ + stroke_done(C, op, stroke); +} + +ViewContext *paint_stroke_view_context(PaintStroke *stroke) +{ + return &stroke->vc; +} + +void *paint_stroke_mode_data(struct PaintStroke *stroke) +{ + return stroke->mode_data; +} + +bool paint_stroke_flipped(struct PaintStroke *stroke) +{ + return stroke->pen_flip; +} + +bool paint_stroke_inverted(struct PaintStroke *stroke) +{ + return stroke->stroke_mode == BRUSH_STROKE_INVERT; +} + +float paint_stroke_distance_get(struct PaintStroke *stroke) +{ + return stroke->stroke_distance; +} + +void paint_stroke_set_mode_data(PaintStroke *stroke, void *mode_data) +{ + stroke->mode_data = mode_data; +} + +bool paint_stroke_started(PaintStroke *stroke) +{ + return stroke->stroke_started; +} + +bool PAINT_brush_tool_poll(bContext *C) +{ + Paint *p = BKE_paint_get_active_from_context(C); + Object *ob = CTX_data_active_object(C); + ScrArea *area = CTX_wm_area(C); + ARegion *region = CTX_wm_region(C); + + if (p && ob && BKE_paint_brush(p) && + (area && ELEM(area->spacetype, SPACE_VIEW3D, SPACE_IMAGE)) && + (region && region->regiontype == RGN_TYPE_WINDOW)) { + /* Check the current tool is a brush. */ + bToolRef *tref = area->runtime.tool; + if (tref && tref->runtime && tref->runtime->data_block[0]) { + return true; + } + } + return false; +} + +void paint_stroke_spline_uv( + PaintStroke *stroke, StrokeCache *cache, const float co[3], float r_out[3], float r_tan[3]) +{ + float3 tan; + float3 p = stroke->world_spline->closest_point(co, r_out[1], tan, r_out[0]); + + r_tan = tan; + r_out[0] = len_v3v3(p, co); + r_out[2] = 0.0f; + + float3 vec = p - float3(co); + float3 vec2; + + cross_v3_v3v3(vec2, vec, tan); + + if (dot_v3v3(vec2, cache->view_normal) < 0.0f) { + r_out[0] = -r_out[0]; + } + + r_out[1] += stroke->stroke_distance_world; +} + +float paint_stroke_spline_length(PaintStroke *stroke) +{ + return stroke->world_spline->length; +} diff --git a/source/blender/editors/sculpt_paint/sculpt.c b/source/blender/editors/sculpt_paint/sculpt.c index 3477285814e..7c43a04549f 100644 --- a/source/blender/editors/sculpt_paint/sculpt.c +++ b/source/blender/editors/sculpt_paint/sculpt.c @@ -2509,6 +2509,35 @@ float SCULPT_brush_strength_factor(SculptSession *ss, avg += br->texture_sample_bias; } + else if (mtex->brush_map_mode == MTEX_MAP_MODE_ROLL) { + float point_3d[3]; + point_3d[2] = 0.0f; + + float tan[3], curv[3]; + + paint_stroke_spline_uv(ss->cache->stroke, ss->cache, brush_point, point_3d, tan); + +#if 0 + if (SCULPT_has_colors(ss)) { + float color[4] = {point_3d[0], point_3d[1], 0.0f, 1.0f}; + mul_v3_fl(color, 0.25f / ss->cache->initial_radius); + color[0] -= floorf(color[0]); + color[1] -= floorf(color[1]); + color[2] -= floorf(color[2]); + + SCULPT_vertex_color_set(ss, vertex, color); + } + +// avg = 0.0f; +#endif + + float pixel_radius = br->size; + mul_v3_fl(point_3d, 1.0f / ss->cache->initial_radius); + + //avg = BKE_brush_sample_tex_3d(scene, br, mtex, point_3d, rgba, thread_id, ss->tex_pool); + avg = paint_get_tex_pixel(mtex, point_3d[0], point_3d[1], ss->tex_pool, thread_id); + avg += br->texture_sample_bias; + } else { const float point_3d[3] = {point_2d[0], point_2d[1], 0.0f}; avg = BKE_brush_sample_tex_3d(scene, br, mtex, point_3d, rgba, 0, ss->tex_pool); @@ -5505,7 +5534,9 @@ static void sculpt_stroke_update_step(bContext *C, const Brush *brush = BKE_paint_brush(&sd->paint); ToolSettings *tool_settings = CTX_data_tool_settings(C); StrokeCache *cache = ss->cache; + cache->stroke_distance = paint_stroke_distance_get(stroke); + cache->stroke = stroke; SCULPT_stroke_modifiers_check(C, ob, brush); sculpt_update_cache_variants(C, sd, ob, itemptr); diff --git a/source/blender/editors/sculpt_paint/sculpt_intern.h b/source/blender/editors/sculpt_paint/sculpt_intern.h index bf47b64d176..4a2088386ba 100644 --- a/source/blender/editors/sculpt_paint/sculpt_intern.h +++ b/source/blender/editors/sculpt_paint/sculpt_intern.h @@ -498,6 +498,8 @@ typedef struct StrokeCache { float true_last_location[3]; float location[3]; float last_location[3]; + + struct PaintStroke *stroke; float stroke_distance; /* Used for alternating between deformation in brushes that need to apply different ones to diff --git a/source/blender/makesdna/DNA_texture_types.h b/source/blender/makesdna/DNA_texture_types.h index c7d49db130e..5c4d2a30b60 100644 --- a/source/blender/makesdna/DNA_texture_types.h +++ b/source/blender/makesdna/DNA_texture_types.h @@ -437,6 +437,7 @@ typedef struct ColorMapping { #define MTEX_MAP_MODE_AREA 3 #define MTEX_MAP_MODE_RANDOM 4 #define MTEX_MAP_MODE_STENCIL 5 +#define MTEX_MAP_MODE_ROLL 6 /* brush_angle_mode */ #define MTEX_ANGLE_RANDOM 1 diff --git a/source/blender/makesrna/intern/rna_brush.c b/source/blender/makesrna/intern/rna_brush.c index 5ba31070e1d..2e8b6f36f4e 100644 --- a/source/blender/makesrna/intern/rna_brush.c +++ b/source/blender/makesrna/intern/rna_brush.c @@ -77,6 +77,7 @@ static const EnumPropertyItem rna_enum_brush_texture_slot_map_all_mode_items[] = {MTEX_MAP_MODE_3D, "3D", 0, "3D", ""}, {MTEX_MAP_MODE_RANDOM, "RANDOM", 0, "Random", ""}, {MTEX_MAP_MODE_STENCIL, "STENCIL", 0, "Stencil", ""}, + {MTEX_MAP_MODE_ROLL, "ROLL", 0, "Roll", ""}, {0, NULL, 0, NULL, NULL}, }; -- cgit v1.2.3