diff --git a/data/kernels/retouch.cl b/data/kernels/retouch.cl index 01a29273c387..a5e526f76316 100644 --- a/data/kernels/retouch.cl +++ b/data/kernels/retouch.cl @@ -59,16 +59,66 @@ retouch_copy_buffer_to_image(global float4 *in, __write_only image2d_t out, global dt_iop_roi_t *roi_out, const int xoffs, - const int yoffs) + const int yoffs, + const float angle, + const float cx, + const float cy) { const int x = get_global_id(0); const int y = get_global_id(1); if(x >= roi_out->width || y >= roi_out->height) return; - if(x + xoffs >= roi_in->width || y + yoffs >= roi_in->height) return; - const int idx = mad24(y + yoffs, roi_in->width, x + xoffs); - write_imagef(out, (int2)(x, y), in[idx]); + // skip the rotation math for negligibly small angles (cheaper, and avoids + // resampling artifacts on a near-zero rotation from repeated UI actions) + if (fabs(angle) < 0.01f) + { + if(x + xoffs >= roi_in->width || y + yoffs >= roi_in->height || x + xoffs < 0 || y + yoffs < 0) return; + const int idx = mad24(y + yoffs, roi_in->width, x + xoffs); + write_imagef(out, (int2)(x, y), in[idx]); + } + else + { + // same rotation as the on-screen source outline, so the overlay marks the + // region actually copied (see rt_copy_in_to_out in retouch.c) + const float c = dtcl_cos(angle); + const float s = dtcl_sin(angle); + // (cx, cy) is the mask centroid (rotation pivot), in roi_out-local coords + const float cx_source = cx + xoffs; + const float cy_source = cy + yoffs; + + const float sx = x + xoffs; + const float sy = y + yoffs; + const float rx = sx - cx_source; + const float ry = sy - cy_source; + const float ix = cx_source + rx * c - ry * s; + const float iy = cy_source + rx * s + ry * c; + + // Edge-clamp (replicate) out-of-bounds samples instead of zero-filling: + // rt_compute_roi_in grows roi_in to the rotation-aware source area, but not + // past the image border, so a rotated source near the edge can still sample + // just outside roi_in. Replicating avoids a hard black seam. + const float ixc = clamp(ix, 0.0f, (float)(roi_in->width - 1)); + const float iyc = clamp(iy, 0.0f, (float)(roi_in->height - 1)); + + int x0 = (int)ixc; + int y0 = (int)iyc; + x0 = clamp(x0, 0, roi_in->width - 2); + y0 = clamp(y0, 0, roi_in->height - 2); + const float dx0 = ixc - x0; + const float dy0 = iyc - y0; + + float4 in00 = in[mad24(y0, roi_in->width, x0)]; + float4 in10 = in[mad24(y0, roi_in->width, x0 + 1)]; + float4 in01 = in[mad24(y0 + 1, roi_in->width, x0)]; + float4 in11 = in[mad24(y0 + 1, roi_in->width, x0 + 1)]; + + float4 val = in00 * (1.0f - dx0) * (1.0f - dy0) + + in10 * dx0 * (1.0f - dy0) + + in01 * (1.0f - dx0) * dy0 + + in11 * dx0 * dy0; + write_imagef(out, (int2)(x, y), val); + } } kernel void @@ -77,15 +127,65 @@ retouch_copy_buffer_to_buffer(global float4 *in, global float4 *out, global dt_iop_roi_t *roi_out, const int xoffs, - const int yoffs) + const int yoffs, + const float angle, + const float cx, + const float cy) { const int x = get_global_id(0); const int y = get_global_id(1); if(x >= roi_out->width || y >= roi_out->height) return; - if(x + xoffs >= roi_in->width || y + yoffs >= roi_in->height) return; - out[mad24(y, roi_out->width, x)] = in[mad24(y + yoffs, roi_in->width, x + xoffs)]; + // skip the rotation math for negligibly small angles (cheaper, and avoids + // resampling artifacts on a near-zero rotation from repeated UI actions) + if (fabs(angle) < 0.01f) + { + if(x + xoffs >= roi_in->width || y + yoffs >= roi_in->height || x + xoffs < 0 || y + yoffs < 0) return; + out[mad24(y, roi_out->width, x)] = in[mad24(y + yoffs, roi_in->width, x + xoffs)]; + } + else + { + // same rotation as the on-screen source outline, so the overlay marks the + // region actually copied (see rt_copy_in_to_out in retouch.c) + const float c = dtcl_cos(angle); + const float s = dtcl_sin(angle); + // (cx, cy) is the mask centroid (rotation pivot), in roi_out-local coords + const float cx_source = cx + xoffs; + const float cy_source = cy + yoffs; + + const float sx = x + xoffs; + const float sy = y + yoffs; + const float rx = sx - cx_source; + const float ry = sy - cy_source; + const float ix = cx_source + rx * c - ry * s; + const float iy = cy_source + rx * s + ry * c; + + // Edge-clamp (replicate) out-of-bounds samples instead of zero-filling: + // rt_compute_roi_in grows roi_in to the rotation-aware source area, but not + // past the image border, so a rotated source near the edge can still sample + // just outside roi_in. Replicating avoids a hard black seam. + const float ixc = clamp(ix, 0.0f, (float)(roi_in->width - 1)); + const float iyc = clamp(iy, 0.0f, (float)(roi_in->height - 1)); + + int x0 = (int)ixc; + int y0 = (int)iyc; + x0 = clamp(x0, 0, roi_in->width - 2); + y0 = clamp(y0, 0, roi_in->height - 2); + const float dx0 = ixc - x0; + const float dy0 = iyc - y0; + + float4 in00 = in[mad24(y0, roi_in->width, x0)]; + float4 in10 = in[mad24(y0, roi_in->width, x0 + 1)]; + float4 in01 = in[mad24(y0 + 1, roi_in->width, x0)]; + float4 in11 = in[mad24(y0 + 1, roi_in->width, x0 + 1)]; + + float4 val = in00 * (1.0f - dx0) * (1.0f - dy0) + + in10 * dx0 * (1.0f - dy0) + + in01 * (1.0f - dx0) * dy0 + + in11 * dx0 * dy0; + out[mad24(y, roi_out->width, x)] = val; + } } kernel void diff --git a/src/develop/masks.h b/src/develop/masks.h index 786a1a80dbfd..5a2112c93587 100644 --- a/src/develop/masks.h +++ b/src/develop/masks.h @@ -346,8 +346,8 @@ typedef struct dt_masks_form_t dt_masks_type_t type; const dt_masks_functions_t *functions; - // position of the source (used only for clone) - float source[2]; + // position of the source (used only for clone). [0]=dx, [1]=dy, [2]=angle + float source[3]; // name of the form char name[128]; // id used to store the form @@ -401,6 +401,14 @@ typedef struct dt_masks_form_gui_t gboolean form_selected; gboolean border_selected; gboolean source_selected; + gboolean source_rotating; + gboolean counter_rotate_source; + // joint rotation grabbed from the source shape: the mouse circles the source, + // so its angular sweep must be measured about the source centroid (not the + // destination centroid) to keep the rotation gain symmetric with grabbing the + // target. The applied angle is identical either way; only the pivot used to + // read the mouse motion differs. + gboolean rotate_about_source; gboolean pivot_selected; gboolean select_only_border; dt_masks_edit_mode_t edit_mode; @@ -1133,6 +1141,26 @@ void dt_masks_closest_point(const int count, float *x, float *y); +/* Rotate the control points of a path/brush outline in screen space and project + them back to normalized image coordinates. `gpt_points` is the gui display + buffer (interleaved x,y) whose first `nb*3` pairs are the control points, + stored per node as ctrl1, corner, ctrl2; `points_count` is its number of + (x,y) pairs. Each control point is rotated by (cos_a, sin_a) around the screen + pivot (cx, cy), back-transformed through the pipe in a single batch, and + written to `out` (normalized, same interleaving, nb*6 floats). Shared by the + path and brush rotate gestures. */ +void dt_masks_rotate_ctrl_points(dt_develop_t *dev, + const float *const gpt_points, + const int points_count, + const int nb, + const float cx, + const float cy, + const float cos_a, + const float sin_a, + const float iwidth, + const float iheight, + float *const out); + /* draw a line from -> to with an arrow at the end. if touch_dest is true then the arrow will be at the (to_x, to_y) location, otherwise a small space will diff --git a/src/develop/masks/brush.c b/src/develop/masks/brush.c index 10d1e30ce190..7c462bc6e7fb 100644 --- a/src/develop/masks/brush.c +++ b/src/develop/masks/brush.c @@ -50,6 +50,32 @@ static inline int _nb_ctrl_point(const int nb_point) return nb_point * 3; } +// Centroid of a brush stroke: the arithmetic mean of `n` dense centerline +// points stored with the given stride (floats between consecutive points; 2 for +// a packed x,y array). A brush "outline" is the centerline traced up then back +// down, so it has no enclosed area -- a shoelace area centroid would be unstable +// there. The mean is rotation-invariant (mean(R*p) = R*mean(p)), so the source +// spins in place; a bounding-box center is not, and would make it drift on +// repeated rotation. +static void _brush_centroid(const float *const pts, + const int n, + const int stride, + float *const ox, + float *const oy) +{ + double mx = 0.0, my = 0.0; + for(int i = 0; i < n; i++) + { + mx += pts[i * stride]; + my += pts[i * stride + 1]; + } + if(n > 0) + { + *ox = (float)(mx / n); + *oy = (float)(my / n); + } +} + /** get squared distance of indexed point to line segment, taking * weighted payload data into account */ static float _brush_point_line_distance2(const int index, @@ -1068,6 +1094,35 @@ static int _brush_get_pts_border(dt_develop_t *dev, ptsbuf[i * 2 + 1] += dy; } + // rotate the source outline by form->source[2] around its centroid. The + // buffer starts with nb*3 control-point entries (ctrl1/corner/ctrl2 per + // node) that are NOT in boundary order, so the pivot is the mean of the + // dense centerline that follows (see _brush_centroid). The centroid is + // rotation-invariant, so the source spins in place; retouch.c rotates the + // cloned/healed source around the centroid of the rasterized mask (which + // approximates this), so the overlay tracks the sampled region. + if(!feqf(form->source[2], 0.0f, 0.01f)) + { + float cx = 0.0f, cy = 0.0f; + const int off = _nb_ctrl_point(nb); + if(*points_count - off >= 3) + _brush_centroid(&ptsbuf[off * 2], *points_count - off, 2, &cx, &cy); + else + _brush_centroid(ptsbuf, *points_count, 2, &cx, &cy); + + const float rc = cosf(form->source[2]); + const float rs = sinf(form->source[2]); + + DT_OMP_FOR(if(*points_count > 100)) + for(int i = 0; i < *points_count; i++) + { + const float rx = ptsbuf[i * 2] - cx; + const float ry = ptsbuf[i * 2 + 1] - cy; + ptsbuf[i * 2] = cx + rx * rc - ry * rs; + ptsbuf[i * 2 + 1] = cy + rx * rs + ry * rc; + } + } + // we apply the rest of the distortions (those after the module) // so we have now the SOURCE points in final image reference if(!dt_dev_distort_transform_plus(dev, pipe, iop_order, @@ -1144,25 +1199,24 @@ static void _brush_get_distance(const float x, // we first check if we are inside the source form - // add support for clone masks - if(gpt->points_count > 2 + _nb_ctrl_point(corner_count) - && gpt->source_count > 2 + _nb_ctrl_point(corner_count)) + // add support for clone masks. Use the source points directly (they already + // carry the source rotation) instead of reconstructing them from the target + // points plus a translation -- otherwise the hit area would ignore the source + // rotation and sit away from the displayed source stroke. + if(gpt->source_count > 2 + _nb_ctrl_point(corner_count)) { - const float dx = -gpt->points[2] + gpt->source[2]; - const float dy = -gpt->points[3] + gpt->source[3]; - int current_seg = 1; - for(int i = _nb_ctrl_point(corner_count); i < gpt->points_count; i++) + for(int i = _nb_ctrl_point(corner_count); i < gpt->source_count; i++) { // do we change of path segment ? - if(gpt->points[i * 2 + 1] == gpt->points[current_seg * 6 + 3] - && gpt->points[i * 2] == gpt->points[current_seg * 6 + 2]) + if(gpt->source[i * 2 + 1] == gpt->source[current_seg * 6 + 3] + && gpt->source[i * 2] == gpt->source[current_seg * 6 + 2]) { current_seg = (current_seg + 1) % corner_count; } - // distance from tested point to current form point - const float yy = gpt->points[i * 2 + 1] + dy; - const float xx = gpt->points[i * 2] + dx; + // distance from tested point to current source point + const float yy = gpt->source[i * 2 + 1]; + const float xx = gpt->source[i * 2]; const float sdx = x - xx; const float sdy = y - yy; @@ -1552,6 +1606,44 @@ static int _brush_events_button_pressed(dt_iop_module_t *module, dt_control_queue_redraw_center(); return 1; } + else if((gui->form_selected || gui->source_selected || (gui->seg_selected >= 0 && gui->point_selected < 0 && + gui->feather_selected < 0 && gui->point_border_selected < 0)) && + gui->edit_mode == DT_MASKS_EDIT_FULL && dt_modifiers_include(state, GDK_CONTROL_MASK)) + { + // Modifier scheme (matches the rotate combo used elsewhere for masks): + // - CTRL: rotate only the shape under the pointer (source or target) + // - CTRL+SHIFT: rotate both shapes together about their centroids + gui->rotate_about_source = FALSE; + if(dt_modifiers_include(state, GDK_SHIFT_MASK)) + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = FALSE; // joint rotation + // If the joint rotation was grabbed on the source, the mouse circles the + // source centroid; measure its angular sweep there so the rotation gain + // matches grabbing the target (see rotate_about_source in masks.h). + gui->rotate_about_source = gui->source_selected; + } + else if(gui->source_selected) + { + gui->source_rotating = TRUE; // source only + } + else + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = TRUE; // target only, source stays put + } + + gui->point_edited = -1; + gui->seg_selected = -1; + + // the rotation pivot is recomputed each motion event from the displayed + // outline (see the form_rotating/source_rotating branches in mouse_moved); + // here we only record the initial mouse position for the incremental delta + gui->scrollx = gui->scrolly = 0.0f; + gui->dx = pzx; + gui->dy = pzy; + return 1; + } else if(gui->source_selected && gui->edit_mode == DT_MASKS_EDIT_FULL) { @@ -1559,8 +1651,15 @@ static int _brush_events_button_pressed(dt_iop_module_t *module, if(!guipt) return 0; // we start the form dragging gui->source_dragging = TRUE; - gui->dx = guipt->source[2] - gui->posx; - gui->dy = guipt->source[3] - gui->posy; + // Anchor the drag on the (unrotated) source position rather than the + // displayed node guipt->source[2]: when the source is rotated + // (source[2] != 0) the displayed node differs from form->source, so + // grabbing it would snap the anchor onto it and make the source jump on + // every click. Use the forward-transformed source position instead. + float anchor[2] = { form->source[0] * iwidth, form->source[1] * iheight }; + dt_dev_distort_transform(darktable.develop, anchor, 1); + gui->dx = anchor[0] - gui->posx; + gui->dy = anchor[1] - gui->posy; return 1; } else if(gui->form_selected @@ -1626,7 +1725,10 @@ static int _brush_events_button_pressed(dt_iop_module_t *module, { const guint nb = g_list_length(form->points); gui->point_edited = -1; - if(dt_modifier_is(state, GDK_CONTROL_MASK) && gui->seg_selected < nb - 1) + // add a node with SHIFT+click on a segment. This used to be CTRL+click, + // but CTRL is now reserved for the rotate gesture (see the rotate branch + // above), so the add-node modifier was moved to SHIFT to avoid a clash. + if(dt_modifier_is(state, GDK_SHIFT_MASK) && gui->seg_selected < nb - 1) { // we add a new point to the brush dt_masks_point_brush_t *bzpt = (malloc(sizeof(dt_masks_point_brush_t))); @@ -2009,6 +2111,28 @@ static int _brush_events_button_released(dt_iop_module_t *module, dt_control_queue_redraw_center(); return 1; } + else if(gui->form_rotating) + { + // rotation was applied incrementally in mouse_moved; just finalise + gui->form_rotating = FALSE; + gui->rotate_about_source = FALSE; + gui->scrollx = gui->scrolly = 0.0f; + + dt_dev_add_masks_history_item(darktable.develop, module, TRUE); + dt_masks_gui_form_create(form, gui, index, module); + + return 1; + } + else if(gui->source_rotating) + { + gui->source_rotating = FALSE; + gui->scrollx = gui->scrolly = 0.0f; + + dt_dev_add_masks_history_item(darktable.develop, module, TRUE); + dt_masks_gui_form_create(form, gui, index, module); + + return 1; + } else if(gui->form_dragging) { // we end the form dragging @@ -2278,6 +2402,147 @@ static int _brush_events_mouse_moved(struct dt_iop_module_t *module, dt_control_queue_redraw_center(); return 1; } + else if(gui->form_rotating) + { + // Rotate in screen (backbuffer pixel) space for perfect visual pivot. + // The outer gpt (fetched at top of function) already has the current display positions. + if(!gpt || gpt->points_count < 6) + { + // not enough data; just swallow the event + gui->dx = pzx; + gui->dy = pzy; + return 1; + } + + // current and previous mouse in backbuffer pixels + const float cmx = pzx * wd; + const float cmy = pzy * ht; + const float pmx = gui->dx * wd; + const float pmy = gui->dy * ht; + + // centroid of the stroke on screen, computed from the dense centerline + // (which follows the nb*3 control-point entries) so the destination spins + // around its rotation-invariant centroid (see the note in + // _brush_get_pts_border). + const int nc = g_list_length(form->points); + const int off = _nb_ctrl_point(nc); + float cx = cmx, cy = cmy; + if(gpt->points_count - off >= 3) + _brush_centroid(&gpt->points[off * 2], gpt->points_count - off, 2, &cx, &cy); + + // Pivot used to read the mouse's angular sweep. For a joint rotation grabbed + // on the source, the mouse circles the source centroid, which is offset from + // the destination centroid; measuring the sweep about the destination would + // under-report the angle (smaller subtended angle at a distant pivot) and + // make the source feel "heavier" to rotate. Measure about the source centroid + // in that case. The destination geometry is still rotated about cx,cy below. + float mx = cx, my = cy; + if(gui->rotate_about_source && gpt->source_count - off >= 3) + _brush_centroid(&gpt->source[off * 2], gpt->source_count - off, 2, &mx, &my); + + float dv = atan2f(cmy - my, cmx - mx) - atan2f(pmy - my, pmx - mx); + if(fabsf(dv) > M_PI_F) + dv -= copysignf(DT_2PI_F, dv); + const float c = cosf(dv); + const float s = sinf(dv); + + // The source is derived from the destination shape (translated by the + // source-to-target offset), so rotating the destination geometry already + // rotates the source by the same amount around its own centroid -- provided + // that offset stays constant. Remember the reference node (first point, which + // anchors the offset) so we can hold the offset fixed below; otherwise the + // source would orbit the destination instead of spinning in place. + dt_masks_point_brush_t *const pt0 = form->points ? form->points->data : NULL; + const float ref_old[2] = { pt0 ? pt0->corner[0] : 0.f, pt0 ? pt0->corner[1] : 0.f }; + + // Rotate each node's three control points (ctrl1, corner, ctrl2) around the + // screen centroid and project them back to image coordinates in one batch, + // then scatter the result into the brush nodes. + float *const npts = dt_alloc_align_float((size_t)nc * 6); + if(npts) + { + dt_masks_rotate_ctrl_points( + darktable.develop, gpt->points, gpt->points_count, nc, cx, cy, c, s, iwidth, iheight, npts); + + int k = 0; + for(GList *l = form->points; l; l = g_list_next(l), k++) + { + dt_masks_point_brush_t *pt = l->data; + pt->ctrl1[0] = npts[k * 6 + 0]; + pt->ctrl1[1] = npts[k * 6 + 1]; + pt->corner[0] = npts[k * 6 + 2]; + pt->corner[1] = npts[k * 6 + 3]; + pt->ctrl2[0] = npts[k * 6 + 4]; + pt->ctrl2[1] = npts[k * 6 + 5]; + } + dt_free_align(npts); + } + + // Source bookkeeping only matters for clone/heal forms (those with a source). + // - joint rotation: both shapes rotate by dv. The source inherits the + // rotation from the destination geometry, so source[2] is left untouched. + // - target-only rotation: cancel that inherited rotation so the source keeps + // its current orientation. + if(form->type & DT_MASKS_CLONE) + { + if(gui->counter_rotate_source) + form->source[2] -= dv; + + // Hold the source-to-target offset constant by shifting the source anchor + // by the displacement of the reference node, keeping the source centroid + // in place (it spins or stays, but never orbits). + if(pt0) + { + form->source[0] += pt0->corner[0] - ref_old[0]; + form->source[1] += pt0->corner[1] - ref_old[1]; + } + } + + // remember current mouse (backbuffer-normalized) for next incremental step + gui->dx = pzx; + gui->dy = pzy; + + dt_masks_gui_form_create(form, gui, index, module); + dt_control_queue_redraw_center(); + return 1; + } + else if(gui->source_rotating) + { + if(!gpt || gpt->source_count < 6) + { + gui->dx = pzx; + gui->dy = pzy; + return 1; + } + + const float cmx = pzx * wd; + const float cmy = pzy * ht; + const float pmx = gui->dx * wd; + const float pmy = gui->dy * ht; + + // Rotate the source around its centroid (mean of the dense centerline, which + // follows the nb*3 control-point entries). The centroid is rotation-invariant, + // so the source spins in place; it is the same pivot used by the source + // display and (approximately) the pixel sampler, so the overlay tracks the + // cloned/healed result. The destination is untouched. + const int off = _nb_ctrl_point(g_list_length(form->points)); + float cx = cmx, cy = cmy; + if(gpt->source_count - off >= 3) + _brush_centroid(&gpt->source[off * 2], gpt->source_count - off, 2, &cx, &cy); + + float dv = atan2f(cmy - cy, cmx - cx) - atan2f(pmy - cy, pmx - cx); + if(fabsf(dv) > M_PI_F) + dv -= copysignf(DT_2PI_F, dv); + + form->source[2] += dv; + + gui->dx = pzx; + gui->dy = pzy; + + dt_masks_gui_form_create(form, gui, index, module); + dt_control_queue_redraw_center(); + return 1; + } else if(gui->form_dragging || gui->source_dragging) { float pts[2] = { pzx * wd + gui->dx, pzy * ht + gui->dy }; @@ -3137,6 +3402,16 @@ static int _brush_get_mask_roi(const dt_iop_module_t *const module, static GSList *_brush_setup_mouse_actions(const struct dt_masks_form_t *const form) { GSList *lm = NULL; + lm = dt_mouse_action_create_simple( + lm, DT_MOUSE_ACTION_LEFT, GDK_SHIFT_MASK, _("[BRUSH on segment] add node")); + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, + GDK_CONTROL_MASK, + _("[BRUSH] rotate shape (source only on a clone source)")); + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, + GDK_SHIFT_MASK | GDK_CONTROL_MASK, + _("[BRUSH] rotate shape and source together")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, 0, _("[BRUSH] change size")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, @@ -3166,9 +3441,32 @@ static void _brush_set_hint_message(const dt_masks_form_gui_t *const gui, // TODO: check if it would be good idea to have same controls on // creation and for selected brush if(gui->creation || gui->form_selected) - g_snprintf(msgbuf, msgbuf_len, + { + g_snprintf(msgbuf, + msgbuf_len, _("size: scroll, hardness: shift+scroll\n" - "opacity: ctrl+scroll (%d%%)"), opacity); + "opacity: ctrl+scroll (%d%%), rotate: ctrl+drag"), + opacity); + // joint rotation of both shapes only makes sense when there is a source + // (clone/heal forms, e.g. in the retouch module), not while still drawing + if(!gui->creation && (form->type & DT_MASKS_CLONE)) + g_strlcat(msgbuf, _(", rotate both: shift+ctrl+drag"), msgbuf_len); + } + else if(gui->source_selected) + // the source only exists for clone/heal forms, so joint rotation always applies here + g_strlcat(msgbuf, + _("move: drag, rotate source: ctrl+drag, " + "rotate both: shift+ctrl+drag"), + msgbuf_len); + else if(gui->seg_selected >= 0) + { + g_strlcat(msgbuf, + _("move segment: drag, add node: shift+click\n" + "rotate: ctrl+drag"), + msgbuf_len); + if(form->type & DT_MASKS_CLONE) + g_strlcat(msgbuf, _(", rotate both: shift+ctrl+drag"), msgbuf_len); + } else if(gui->border_selected) g_strlcat(msgbuf, _("size: scroll"), msgbuf_len); } diff --git a/src/develop/masks/ellipse.c b/src/develop/masks/ellipse.c index f3d1847178e6..775555da3ea1 100644 --- a/src/develop/masks/ellipse.c +++ b/src/develop/masks/ellipse.c @@ -394,8 +394,11 @@ static int _ellipse_get_points_border(dt_develop_t *dev, if(source) { const float xs = form->source[0], ys = form->source[1]; + // the source content is sampled rotated by form->source[2] (radians), + // so the source outline must reflect that extra rotation return _ellipse_get_points_source(dev, x, y, xs, ys, a, b, - ellipse->rotation, points, points_count, module); + ellipse->rotation + rad2degf(form->source[2]), + points, points_count, module); } else { @@ -585,7 +588,7 @@ static int _ellipse_events_button_pressed(dt_iop_module_t *module, } else if(gui->edit_mode == DT_MASKS_EDIT_FULL) { - if(gui->source_selected) + if(gui->source_selected && !dt_modifiers_include(state, GDK_CONTROL_MASK)) { gui->dx = gpt->source[0] - gui->posx; gui->dy = gpt->source[1] - gui->posy; @@ -597,9 +600,38 @@ static int _ellipse_events_button_pressed(dt_iop_module_t *module, gui->dx = gpt->points[0] - gui->posx; gui->dy = gpt->points[1] - gui->posy; - if(gui->form_selected && dt_modifier_is(state, GDK_CONTROL_MASK)) + if((gui->form_selected || gui->source_selected) && dt_modifiers_include(state, GDK_CONTROL_MASK)) { - gui->form_rotating = TRUE; + // Modifier scheme (matches the rotate combo used elsewhere for masks): + // - CTRL: rotate only the shape under the pointer (source or target) + // - CTRL+SHIFT: rotate both shapes together + gui->rotate_about_source = FALSE; + if(dt_modifiers_include(state, GDK_SHIFT_MASK)) + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = FALSE; // joint rotation + // If the joint rotation was grabbed on the source, the mouse circles + // the source center; measure its angular sweep there so the rotation + // gain matches grabbing the target (see rotate_about_source in masks.h). + gui->rotate_about_source = gui->source_selected; + if(gui->source_selected) + { + gui->dx = gpt->source[0] - gui->posx; + gui->dy = gpt->source[1] - gui->posy; + } + } + else if(gui->source_selected) + { + gui->source_rotating = TRUE; // source only + // rotate around the source center, so use it as reference point + gui->dx = gpt->source[0] - gui->posx; + gui->dy = gpt->source[1] - gui->posy; + } + else + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = TRUE; // target only, source stays put + } return 1; } else if(gui->point_selected >= 1) @@ -875,9 +907,11 @@ static int _ellipse_events_button_released(dt_iop_module_t *module, dt_masks_form_gui_points_t *gpt = g_list_nth_data(gui->points, index); if(!gpt) return 0; - // ellipse center - const float xref = gpt->points[0]; - const float yref = gpt->points[1]; + // pivot for reading the final mouse sweep: the center of the shape the mouse + // is circling (source for a joint rotation grabbed on the source), matching + // the mouse_moved branch above. + const float xref = gui->rotate_about_source ? gpt->source[0] : gpt->points[0]; + const float yref = gui->rotate_about_source ? gpt->source[1] : gpt->points[1]; const float pts[8] = { xref, yref, x , y, 0, 0, gui->dx, gui->dy }; @@ -898,7 +932,16 @@ static int _ellipse_events_button_released(dt_iop_module_t *module, else ellipse->rotation += rad2degf(dv); + // Rotation behavior (counter_rotate_source is set at button-press time): + // - CTRL only (target): only the target rotates, the source stays fixed + // (counter_rotate_source == TRUE, so the branch below is skipped) + // - CTRL+SHIFT: both shapes rotate together by the same amount + // (counter_rotate_source == FALSE, so the source angle follows) + if(!gui->counter_rotate_source) + form->source[2] += dv; + dt_conf_set_float(DT_MASKS_CONF(form->type, ellipse, rotation), ellipse->rotation); + gui->rotate_about_source = FALSE; dt_dev_add_masks_history_item(darktable.develop, module, TRUE); @@ -907,6 +950,33 @@ static int _ellipse_events_button_released(dt_iop_module_t *module, return 1; } + else if(gui->source_rotating && gui->edit_mode == DT_MASKS_EDIT_FULL) + { + gui->source_rotating = FALSE; + + const float x = pzx * wd; + const float y = pzy * ht; + + dt_masks_form_gui_points_t *gpt = g_list_nth_data(gui->points, index); + if(!gpt) return 0; + + const float xref = gpt->source[0]; + const float yref = gpt->source[1]; + + const float pts[8] = { xref, yref, x , y, 0, 0, gui->dx, gui->dy }; + + const float dv = atan2f(pts[3] - pts[1], + pts[2] - pts[0]) - atan2f(-(pts[7] - pts[5]), + -(pts[6] - pts[4])); + + form->source[2] += dv; + + dt_dev_add_masks_history_item(darktable.develop, module, TRUE); + + dt_masks_gui_form_create(form, gui, index, module); + + return 1; + } else if(gui->point_dragging >= 1 && gui->edit_mode == DT_MASKS_EDIT_FULL) { @@ -1100,9 +1170,14 @@ static int _ellipse_events_mouse_moved(dt_iop_module_t *module, dt_masks_form_gui_points_t *gpt = g_list_nth_data(gui->points, index); if(!gpt) return 0; - // ellipse center - const float xref = gpt->points[0]; - const float yref = gpt->points[1]; + // Pivot used to read the mouse's angular sweep: the center of the shape the + // mouse is circling. For a joint rotation grabbed on the source, that is the + // source center; measuring about the destination center would under-report + // the angle (smaller subtended angle at a distant pivot) and make the source + // feel "heavier" to rotate. The rotation amount applied to both shapes is the + // same scalar either way. + const float xref = gui->rotate_about_source ? gpt->source[0] : gpt->points[0]; + const float yref = gui->rotate_about_source ? gpt->source[1] : gpt->points[1]; const float pts[8] = { xref, yref, x, y, 0, 0, gui->dx, gui->dy }; @@ -1117,10 +1192,18 @@ static int _ellipse_events_mouse_moved(dt_iop_module_t *module, pts2[4] - pts2[0]); // Normalize to the range -180 to 180 degrees check_angle = atan2f(sinf(check_angle), cosf(check_angle)); - if(check_angle < 0) - ellipse->rotation -= rad2degf(dv); - else - ellipse->rotation += rad2degf(dv); + + float diff = (check_angle < 0) ? -rad2degf(dv) : rad2degf(dv); + + // Rotation behavior (counter_rotate_source is set at button-press time): + // - CTRL only (target): only the target rotates, the source stays fixed + // (counter_rotate_source == TRUE, so the branch below is skipped) + // - CTRL+SHIFT: both shapes rotate together by the same amount + // (counter_rotate_source == FALSE, so the source angle follows) + if(!gui->counter_rotate_source) + form->source[2] += deg2radf(diff); + + ellipse->rotation += diff; dt_conf_set_float(DT_MASKS_CONF(form->type, ellipse, rotation), ellipse->rotation); @@ -1128,8 +1211,46 @@ static int _ellipse_events_mouse_moved(dt_iop_module_t *module, dt_masks_gui_form_create(form, gui, index, module); // we remap dx, dy to the right values, as it will be used in next movements - gui->dx = xref - gui->posx; - gui->dy = yref - gui->posy; + gui->dx = xref - x; + gui->dy = yref - y; + + dt_control_queue_redraw_center(); + return 1; + } + else if(gui->source_rotating) + { + float wd, ht, iwidth, iheight; + dt_masks_get_image_size(&wd, &ht, &iwidth, &iheight); + const float x = pzx * wd; + const float y = pzy * ht; + + dt_masks_form_gui_points_t *gpt = g_list_nth_data(gui->points, index); + if(!gpt) return 0; + + const float xref = gpt->source[0]; + const float yref = gpt->source[1]; + + const float pts[8] = { xref, yref, x, y, 0, 0, gui->dx, gui->dy }; + + const float dv = atan2f(pts[3] - pts[1], pts[2] - pts[0]) + - atan2f(-(pts[7] - pts[5]), -(pts[6] - pts[4])); + + float pts2[8] = { xref, yref, x, y, xref + 10.0f, yref, xref, yref + 10.0f }; + dt_dev_distort_backtransform(darktable.develop, pts2, 4); + + float check_angle = atan2f(pts2[7] - pts2[1], + pts2[6] - pts2[0]) - atan2f(pts2[5] - pts2[1], + pts2[4] - pts2[0]); + // Normalize to the range -180 to 180 degrees + check_angle = atan2f(sinf(check_angle), cosf(check_angle)); + + float diff = (check_angle < 0) ? -dv : dv; + form->source[2] += diff; + + dt_masks_gui_form_create(form, gui, index, module); + + gui->dx = xref - x; + gui->dy = yref - y; dt_control_queue_redraw_center(); return 1; @@ -1965,9 +2086,14 @@ static GSList *_ellipse_setup_mouse_actions(const struct dt_masks_form_t *const lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT, GDK_SHIFT_MASK, _("[ELLIPSE] switch feathering mode")); - lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT_DRAG, + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, GDK_CONTROL_MASK, - _("[ELLIPSE] rotate shape")); + _("[ELLIPSE] rotate shape (source only on a clone source)")); + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, + GDK_SHIFT_MASK | GDK_CONTROL_MASK, + _("[ELLIPSE] rotate shape and source together")); return lm; } @@ -2016,11 +2142,23 @@ static void _ellipse_set_hint_message(const dt_masks_form_gui_t *const gui, "opacity: ctrl+scroll (%d%%)"), opacity); else if(gui->point_selected >= 0) g_strlcat(msgbuf, _("rotate: ctrl+drag"), msgbuf_len); + else if(gui->source_selected) + // the source only exists for clone/heal forms, so joint rotation always applies here + g_strlcat(msgbuf, + _("move: drag, rotate source: ctrl+drag, " + "rotate both: shift+ctrl+drag"), + msgbuf_len); else if(gui->form_selected) + { g_snprintf(msgbuf, msgbuf_len, _("feather mode: alt+click, rotate: ctrl+drag\n" "size: scroll, feather size: shift+scroll," " opacity: ctrl+scroll (%d%%)"), opacity); + // joint rotation of both shapes only makes sense when there is a source + // (clone/heal forms, e.g. in the retouch module) + if(form->type & DT_MASKS_CLONE) + g_strlcat(msgbuf, _("\nrotate both: shift+ctrl+drag"), msgbuf_len); + } } static void _ellipse_sanitize_config(const dt_masks_type_t type) diff --git a/src/develop/masks/gradient.c b/src/develop/masks/gradient.c index 42bdbf78895b..1fbe6a81330c 100644 --- a/src/develop/masks/gradient.c +++ b/src/develop/masks/gradient.c @@ -231,7 +231,10 @@ static int _gradient_events_button_pressed(dt_iop_module_t *module, const dt_masks_form_gui_points_t *gpt = g_list_nth_data(gui->points, index); if(!gpt) return 0; // we start the form rotating or dragging - if(gui->pivot_selected) + // - dragging the pivot handle rotates (legacy gesture) + // - CTRL+drag anywhere rotates too, matching the rotate convention + // used by the other masks (ellipse, path, brush) + if(gui->pivot_selected || dt_modifier_is(state, GDK_CONTROL_MASK)) gui->form_rotating = TRUE; else gui->form_dragging = TRUE; @@ -1416,6 +1419,8 @@ static GSList *_gradient_setup_mouse_actions(const dt_masks_form_t *const form) GSList *lm = NULL; lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT_DRAG, 0, _("[GRADIENT on pivot] rotate shape")); + lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT_DRAG, + GDK_CONTROL_MASK, _("[GRADIENT] rotate shape")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT_DRAG, 0, _("[GRADIENT creation] set rotation")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, @@ -1453,7 +1458,7 @@ static void _gradient_set_hint_message(const dt_masks_form_gui_t *const gui, else if(gui->form_selected) g_snprintf(msgbuf, msgbuf_len, _("curvature: scroll, compression: shift+scroll\n" - "opacity: ctrl+scroll (%d%%)"), opacity); + "rotate: ctrl+drag, opacity: ctrl+scroll (%d%%)"), opacity); else if(gui->pivot_selected) g_strlcat(msgbuf, _("rotate: drag"), msgbuf_len); } diff --git a/src/develop/masks/group.c b/src/develop/masks/group.c index d293845d4964..645a1377afc4 100644 --- a/src/develop/masks/group.c +++ b/src/develop/masks/group.c @@ -67,6 +67,8 @@ static int _group_events_button_pressed(dt_iop_module_t *module, gui->border_selected = FALSE; gui->form_dragging = FALSE; gui->form_rotating = FALSE; + gui->source_rotating = FALSE; + gui->counter_rotate_source = FALSE; gui->pivot_selected = FALSE; gui->point_border_selected = -1; gui->seg_selected = -1; @@ -133,6 +135,7 @@ static inline gboolean _is_handling_form(dt_masks_form_gui_t *gui) || gui->source_dragging || gui->gradient_toggling || gui->form_rotating + || gui->source_rotating || (gui->point_edited != -1) || (gui->point_dragging != -1) || (gui->feather_dragging != -1) diff --git a/src/develop/masks/masks.c b/src/develop/masks/masks.c index 031eb52e104e..bebf99f6d044 100644 --- a/src/develop/masks/masks.c +++ b/src/develop/masks/masks.c @@ -165,7 +165,10 @@ static void _set_hinter_message(const dt_masks_form_gui_t *gui, if(sel->functions && sel->functions->set_hint_message) { - sel->functions->set_hint_message(gui, form, opacity, msg, sizeof(msg)); + // pass the selected sub-form (sel), not the outer form: when editing a + // group member, `form` is the group, so its type/points would not describe + // the shape the hint is about (e.g. whether it is a clone with a source) + sel->functions->set_hint_message(gui, sel, opacity, msg, sizeof(msg)); } dt_control_hinter_message(msg); @@ -428,6 +431,7 @@ int dt_masks_form_duplicate(dt_develop_t *dev, const dt_mask_id_t formid) // we copy the base values fdest->source[0] = fbase->source[0]; fdest->source[1] = fbase->source[1]; + fdest->source[2] = fbase->source[2]; fdest->version = fbase->version; snprintf(fdest->name, sizeof(fdest->name), _("copy of `%s'"), fbase->name); @@ -971,7 +975,25 @@ void dt_masks_read_masks_history(dt_develop_t *dev, const dt_imgid_t imgid) form->version = sqlite3_column_int(stmt, 4); form->points = NULL; const int nb_points = sqlite3_column_int(stmt, 6); - memcpy(form->source, sqlite3_column_blob(stmt, 7), sizeof(float) * 2); + const int source_bytes = sqlite3_column_bytes(stmt, 7); + if(source_bytes == sizeof(float) * 2) + { + memcpy(form->source, sqlite3_column_blob(stmt, 7), sizeof(float) * 2); + form->source[2] = 0.0f; + } + else if(source_bytes == sizeof(float) * 3) + { + memcpy(form->source, sqlite3_column_blob(stmt, 7), sizeof(float) * 3); + } + else if(source_bytes == sizeof(float) * 4) + { + // migration: old format stored an unused scale field as source[3]; drop it + memcpy(form->source, sqlite3_column_blob(stmt, 7), sizeof(float) * 3); + } + else + { + memset(form->source, 0, sizeof(float) * 3); + } // and now we "read" the blob if(form->functions) @@ -1060,7 +1082,7 @@ void dt_masks_write_masks_history_item(const dt_imgid_t imgid, DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, form->formid); DT_DEBUG_SQLITE3_BIND_INT(stmt, 3, form->type); DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 4, form->name, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_BLOB(stmt, 8, form->source, 2 * sizeof(float), SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_BLOB(stmt, 8, form->source, 3 * sizeof(float), SQLITE_TRANSIENT); DT_DEBUG_SQLITE3_BIND_INT(stmt, 5, form->version); if(form->functions) { @@ -1332,6 +1354,8 @@ void dt_masks_clear_form_gui(const dt_develop_t *dev) dev->form_gui->form_dragging = dev->form_gui->form_rotating = dev->form_gui->border_toggling = dev->form_gui->gradient_toggling = FALSE; dev->form_gui->source_selected = dev->form_gui->source_dragging = FALSE; + dev->form_gui->source_rotating = dev->form_gui->counter_rotate_source = FALSE; + dev->form_gui->rotate_about_source = FALSE; dev->form_gui->pivot_selected = FALSE; dev->form_gui->point_border_selected = dev->form_gui->seg_selected = dev->form_gui->point_selected = dev->form_gui->feather_selected = -1; @@ -2045,7 +2069,7 @@ dt_hash_t dt_masks_group_hash(dt_hash_t hash, dt_masks_form_t *form) hash = dt_hash(hash, &form->type, sizeof(dt_masks_type_t)); hash = dt_hash(hash, &form->formid, sizeof(dt_mask_id_t)); hash = dt_hash(hash, &form->version, sizeof(int)); - hash = dt_hash(hash, &form->source, sizeof(float) * 2); + hash = dt_hash(hash, &form->source, sizeof(float) * 3); for(const GList *forms = form->points; forms; forms = g_list_next(forms)) { @@ -2786,6 +2810,49 @@ void dt_masks_closest_point(const int count, } } +void dt_masks_rotate_ctrl_points(dt_develop_t *dev, + const float *const gpt_points, + const int points_count, + const int nb, + const float cx, + const float cy, + const float cos_a, + const float sin_a, + const float iwidth, + const float iheight, + float *const out) +{ + // the control points are the first nb*3 (x,y) pairs of the display buffer + const int nctrl = nb * 3; + if(nctrl < 1 || points_count < nctrl) + return; + + // rotate every control point around the screen pivot into a scratch buffer, + // then back-transform the whole batch in one pipe traversal (cheaper and more + // accurate than inverting the pipe per point) + float *const scr = dt_alloc_align_float((size_t)nctrl * 2); + if(!scr) + return; + + for(int i = 0; i < nctrl; i++) + { + const float rx = gpt_points[i * 2] - cx; + const float ry = gpt_points[i * 2 + 1] - cy; + scr[i * 2] = cx + rx * cos_a - ry * sin_a; + scr[i * 2 + 1] = cy + rx * sin_a + ry * cos_a; + } + + dt_dev_distort_backtransform(dev, scr, nctrl); + + for(int i = 0; i < nctrl; i++) + { + out[i * 2] = scr[i * 2] / iwidth; + out[i * 2 + 1] = scr[i * 2 + 1] / iheight; + } + + dt_free_align(scr); +} + void dt_masks_line_stroke(cairo_t *cr, const gboolean border, const gboolean source, diff --git a/src/develop/masks/path.c b/src/develop/masks/path.c index 4da6a3e5ff90..33e9912cb203 100644 --- a/src/develop/masks/path.c +++ b/src/develop/masks/path.c @@ -28,6 +28,44 @@ #include "develop/openmp_maths.h" #include +// Polygon area centroid (shoelace) of `n` points stored with the given stride +// (number of floats between consecutive points; 2 for a packed x,y array). The +// area centroid is rotation-invariant (rotating the shape about it leaves it +// fixed), which is exactly what the clone/heal source rotation needs: a +// bounding-box center would shift every time the shape is re-rotated and make +// the source drift. Falls back to the arithmetic mean for a degenerate +// (near-zero-area) polygon. +static void _path_centroid(const float *const pts, + const int n, + const int stride, + float *const ox, + float *const oy) +{ + double a = 0.0, cx = 0.0, cy = 0.0, mx = 0.0, my = 0.0; + for(int i = 0; i < n; i++) + { + const int j = (i + 1) % n; + const double xi = pts[i * stride], yi = pts[i * stride + 1]; + const double xj = pts[j * stride], yj = pts[j * stride + 1]; + const double cr = xi * yj - xj * yi; + a += cr; + cx += (xi + xj) * cr; + cy += (yi + yj) * cr; + mx += xi; + my += yi; + } + if(fabs(a) > 1e-9) + { + *ox = (float)(cx / (3.0 * a)); + *oy = (float)(cy / (3.0 * a)); + } + else if(n > 0) + { + *ox = (float)(mx / n); + *oy = (float)(my / n); + } +} + static void _path_bounding_box_raw(const float *const points, const float *border, const int nb_corner, @@ -1514,6 +1552,38 @@ static int _path_get_pts_border(dt_develop_t *dev, ptsbuf[i * 2 + 1] += dy; } + // rotate the source outline by form->source[2] around its area centroid. + // The centroid is rotation-invariant, so re-rotating (or rotating the + // target, which the source geometry inherits) keeps the source in place; + // retouch.c rotates the cloned/healed source around the centroid of the + // rasterized mask, which approximates this outline centroid (they differ + // only by the feather), so the overlay tracks the sampled region. + if(!feqf(form->source[2], 0.0f, 0.01f)) + { + // area centroid of the source = rotation pivot. The buffer starts with + // nb*3 control-point entries (ctrl1/corner/ctrl2 per node) that are NOT + // in boundary order, so only the dense outline that follows is a valid + // polygon for the shoelace centroid. + float cx = 0.0f, cy = 0.0f; + const int off = _nb_wctrl_points(nb); + if(*points_count - off >= 3) + _path_centroid(&ptsbuf[off * 2], *points_count - off, 2, &cx, &cy); + else + _path_centroid(ptsbuf, *points_count, 2, &cx, &cy); + + const float rc = cosf(form->source[2]); + const float rs = sinf(form->source[2]); + + DT_OMP_FOR(if(*points_count > 100)) + for(int i = 0; i < *points_count; i++) + { + const float rx = ptsbuf[i * 2] - cx; + const float ry = ptsbuf[i * 2 + 1] - cy; + ptsbuf[i * 2] = cx + rx * rc - ry * rs; + ptsbuf[i * 2 + 1] = cy + rx * rs + ry * rc; + } + } + // we apply the rest of the distortions (those after the module) // so we have now the SOURCE points in final image reference if(!dt_dev_distort_transform_plus(dev, pipe, iop_order, @@ -2103,14 +2173,59 @@ static int _path_events_button_pressed(dt_iop_module_t *module, dt_control_queue_redraw_center(); return 1; } + else if((gui->form_selected || gui->source_selected) && gui->edit_mode == DT_MASKS_EDIT_FULL && + dt_modifiers_include(state, GDK_CONTROL_MASK)) + { + // Modifier scheme (matches the rotate combo used elsewhere for masks): + // - CTRL: rotate only the shape under the pointer (source or target) + // - CTRL+SHIFT: rotate both shapes together about their centroids + gui->rotate_about_source = FALSE; + if(dt_modifiers_include(state, GDK_SHIFT_MASK)) + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = FALSE; // joint rotation + // If the joint rotation was grabbed on the source, the mouse circles the + // source centroid; measure its angular sweep there so the rotation gain + // matches grabbing the target (see rotate_about_source in masks.h). + gui->rotate_about_source = gui->source_selected; + } + else if(gui->source_selected) + { + gui->source_rotating = TRUE; // source only + } + else + { + gui->form_rotating = TRUE; + gui->counter_rotate_source = TRUE; // target only, source stays put + } + + gui->point_edited = -1; + gui->seg_selected = -1; + + // the rotation pivot is recomputed each motion event from the displayed + // outline (see the form_rotating/source_rotating branches in mouse_moved); + // here we only record the initial mouse position for the incremental delta + gui->scrollx = gui->scrolly = 0.0f; + gui->dx = pzx; + gui->dy = pzy; + return 1; + } else if(gui->source_selected && gui->edit_mode == DT_MASKS_EDIT_FULL) { if(!gpt) return 0; // we start the form dragging gui->source_dragging = TRUE; gui->point_edited = -1; - gui->dx = gpt->source[2] - gui->posx; - gui->dy = gpt->source[3] - gui->posy; + // Anchor the drag on the (unrotated) source position rather than the + // displayed corner gpt->source[2]: when the source is rotated + // (source[2] != 0) the displayed corner differs from form->source, so + // grabbing the corner would snap the anchor onto it and make the source + // jump on every click. Use the forward-transformed source position so the + // offset is computed against the same point that form->source represents. + float anchor[2] = { form->source[0] * iwidth, form->source[1] * iheight }; + dt_dev_distort_transform(darktable.develop, anchor, 1); + gui->dx = anchor[0] - gui->posx; + gui->dy = anchor[1] - gui->posy; return 1; } else if(gui->form_selected && gui->edit_mode == DT_MASKS_EDIT_FULL) @@ -2199,7 +2314,10 @@ static int _path_events_button_pressed(dt_iop_module_t *module, else if(gui->seg_selected >= 0) { gui->point_edited = -1; - if(dt_modifier_is(state, GDK_CONTROL_MASK)) + // add a node with SHIFT+click on a segment. This used to be CTRL+click, + // but CTRL is now reserved for the rotate gesture, and SHIFT matches the + // add-node modifier used by brushes. + if(dt_modifier_is(state, GDK_SHIFT_MASK)) { // we add a new point to the path dt_masks_point_path_t *bzpt = malloc(sizeof(dt_masks_point_path_t)); @@ -2404,6 +2522,28 @@ static int _path_events_button_released(dt_iop_module_t *module, return 1; } + else if(gui->form_rotating) + { + // rotation was applied incrementally in mouse_moved; just finalise + gui->form_rotating = FALSE; + gui->rotate_about_source = FALSE; + gui->scrollx = gui->scrolly = 0.0f; + + dt_dev_add_masks_history_item(darktable.develop, module, TRUE); + dt_masks_gui_form_create(form, gui, index, module); + + return 1; + } + else if(gui->source_rotating) + { + gui->source_rotating = FALSE; + gui->scrollx = gui->scrolly = 0.0f; + + dt_dev_add_masks_history_item(darktable.develop, module, TRUE); + dt_masks_gui_form_create(form, gui, index, module); + + return 1; + } else if(gui->source_dragging) { // we end the form dragging @@ -2690,6 +2830,147 @@ static int _path_events_mouse_moved(dt_iop_module_t *module, dt_control_queue_redraw_center(); return 1; } + else if(gui->source_rotating) + { + if(!gpt || gpt->source_count < 6) + { + gui->dx = pzx; + gui->dy = pzy; + return 1; + } + + const float cmx = pzx * wd; + const float cmy = pzy * ht; + const float pmx = gui->dx * wd; + const float pmy = gui->dy * ht; + + // Rotate the source around its area centroid (computed from the dense + // outline, which follows the nb*3 control-point entries). The centroid is + // rotation-invariant, so the source spins in place; it is the same pivot used + // by the source display and (approximately) the pixel sampler, so the overlay + // tracks the cloned/healed result. The destination is untouched. + const int off = _nb_wctrl_points(g_list_length(form->points)); + float cx = cmx, cy = cmy; + if(gpt->source_count - off >= 3) + _path_centroid(&gpt->source[off * 2], gpt->source_count - off, 2, &cx, &cy); + + float dv = atan2f(cmy - cy, cmx - cx) - atan2f(pmy - cy, pmx - cx); + if(fabsf(dv) > M_PI_F) + dv -= copysignf(DT_2PI_F, dv); + + form->source[2] += dv; + + gui->dx = pzx; + gui->dy = pzy; + + dt_masks_gui_form_create(form, gui, index, module); + dt_control_queue_redraw_center(); + return 1; + } + else if(gui->form_rotating) + { + // Rotate in screen (backbuffer pixel) space for perfect visual pivot. + // The outer gpt (fetched at top of function) already has the current display positions. + if(!gpt || gpt->points_count < 6) + { + // not enough data; just swallow the event + gui->dx = pzx; + gui->dy = pzy; + return 1; + } + + // current and previous mouse in backbuffer pixels + const float cmx = pzx * wd; + const float cmy = pzy * ht; + const float pmx = gui->dx * wd; + const float pmy = gui->dy * ht; + + // area centroid of the shape on screen, computed from the dense outline + // (which follows the nb*3 control-point entries) so the destination spins + // around its rotation-invariant centroid -- the same point the source and + // the rasterized mask rotate about (see the note in _path_get_pts_border). + const int nc = g_list_length(form->points); + const int off = _nb_wctrl_points(nc); + float cx = cmx, cy = cmy; + if(gpt->points_count - off >= 3) + _path_centroid(&gpt->points[off * 2], gpt->points_count - off, 2, &cx, &cy); + + // Pivot used to read the mouse's angular sweep. For a joint rotation grabbed + // on the source, the mouse circles the source centroid, which is offset from + // the destination centroid; measuring the sweep about the destination would + // under-report the angle (smaller subtended angle at a distant pivot) and + // make the source feel "heavier" to rotate. Measure about the source centroid + // in that case. The destination geometry is still rotated about cx,cy below. + float mx = cx, my = cy; + if(gui->rotate_about_source && gpt->source_count - off >= 3) + _path_centroid(&gpt->source[off * 2], gpt->source_count - off, 2, &mx, &my); + + float dv = atan2f(cmy - my, cmx - mx) - atan2f(pmy - my, pmx - mx); + if(fabsf(dv) > M_PI_F) + dv -= copysignf(DT_2PI_F, dv); + const float c = cosf(dv); + const float s = sinf(dv); + + // The source is derived from the destination shape (translated by the + // source-to-target offset), so rotating the destination geometry already + // rotates the source by the same amount around its own centroid -- provided + // that offset stays constant. Remember the reference corner (first node, + // which anchors the offset) so we can hold the offset fixed below; otherwise + // the source would orbit the destination instead of spinning in place. + dt_masks_point_path_t *const pt0 = form->points ? form->points->data : NULL; + const float ref_old[2] = { pt0 ? pt0->corner[0] : 0.f, pt0 ? pt0->corner[1] : 0.f }; + + // Rotate each node's three control points (ctrl1, corner, ctrl2) around the + // screen centroid and project them back to image coordinates in one batch, + // then scatter the result into the path nodes. + float *const npts = dt_alloc_align_float((size_t)nc * 6); + if(npts) + { + dt_masks_rotate_ctrl_points( + darktable.develop, gpt->points, gpt->points_count, nc, cx, cy, c, s, iwidth, iheight, npts); + + int k = 0; + for(GList *l = form->points; l; l = g_list_next(l), k++) + { + dt_masks_point_path_t *pt = l->data; + pt->ctrl1[0] = npts[k * 6 + 0]; + pt->ctrl1[1] = npts[k * 6 + 1]; + pt->corner[0] = npts[k * 6 + 2]; + pt->corner[1] = npts[k * 6 + 3]; + pt->ctrl2[0] = npts[k * 6 + 4]; + pt->ctrl2[1] = npts[k * 6 + 5]; + } + dt_free_align(npts); + } + + // Source bookkeeping only matters for clone/heal forms (those with a source). + // - joint rotation: both shapes rotate by dv. The source inherits the + // rotation from the destination geometry, so source[2] is left untouched. + // - target-only rotation: cancel that inherited rotation so the source keeps + // its current orientation. + if(form->type & DT_MASKS_CLONE) + { + if(gui->counter_rotate_source) + form->source[2] -= dv; + + // Hold the source-to-target offset constant by shifting the source anchor + // by the displacement of the reference corner. This keeps the source + // centroid in place (it spins or stays, but never orbits). + if(pt0) + { + form->source[0] += pt0->corner[0] - ref_old[0]; + form->source[1] += pt0->corner[1] - ref_old[1]; + } + } + + // remember current mouse (backbuffer-normalized) for next incremental step + gui->dx = pzx; + gui->dy = pzy; + + dt_masks_gui_form_create(form, gui, index, module); + dt_control_queue_redraw_center(); + return 1; + } gui->form_selected = FALSE; gui->border_selected = FALSE; @@ -2776,7 +3057,12 @@ static int _path_events_mouse_moved(dt_iop_module_t *module, int near = 0; float dist = 0; _path_get_distance(pzx, pzy, as, gui, index, nb, &in, &inb, &near, &ins, &dist); - gui->seg_selected = !gui->select_only_border && dist < sqf(as) ? near : -1; + // Note: segment selection is intentionally NOT gated on select_only_border + // (SHIFT). SHIFT now also means "add a node on a segment" (matching brushes), + // and SHIFT is what sets select_only_border -- gating here would make the + // segment deselect under SHIFT and break add-node. Border handles are matched + // earlier (and return first), so this only enables SHIFT+click add-node. + gui->seg_selected = dist < sqf(as) ? near : -1; // no segment selected, set form or source selection if(near < 0) @@ -3951,8 +4237,16 @@ static GSList *_path_setup_mouse_actions(const dt_masks_form_t *const form) _("[PATH on node] remove the node")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_RIGHT, 0, _("[PATH on feather] reset curvature")); - lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_LEFT, GDK_CONTROL_MASK, - _("[PATH on segment] add node")); + lm = dt_mouse_action_create_simple( + lm, DT_MOUSE_ACTION_LEFT, GDK_SHIFT_MASK, _("[PATH on segment] add node")); + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, + GDK_CONTROL_MASK, + _("[PATH] rotate shape (source only on a clone source)")); + lm = dt_mouse_action_create_simple(lm, + DT_MOUSE_ACTION_LEFT_DRAG, + GDK_SHIFT_MASK | GDK_CONTROL_MASK, + _("[PATH] rotate shape and source together")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, 0, _("[PATH] change size")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, @@ -4001,15 +4295,27 @@ static void _path_set_hint_message(const dt_masks_form_gui_t *const gui, msgbuf_len); else if(gui->seg_selected >= 0) g_strlcat(msgbuf, - _("move segment: drag, add node: ctrl+click\n" + _("move segment: drag, add node: shift+click\n" "remove path: right-click"), msgbuf_len); + else if(gui->source_selected) + // the source only exists for clone/heal forms, so joint rotation always applies here + g_strlcat(msgbuf, + _("move: drag, rotate source: ctrl+drag, " + "rotate both: shift+ctrl+drag"), + msgbuf_len); else if(gui->form_selected) + { g_snprintf(msgbuf, msgbuf_len, _("size: scroll, feather size: shift+scroll\n" - "opacity: ctrl+scroll (%d%%)"), + "opacity: ctrl+scroll (%d%%), rotate: ctrl+drag"), opacity); + // joint rotation of both shapes only makes sense when there is a source + // (clone/heal forms, e.g. in the retouch module) + if(form->type & DT_MASKS_CLONE) + g_strlcat(msgbuf, _(", rotate both: shift+ctrl+drag"), msgbuf_len); + } } static void _path_duplicate_points(dt_develop_t *const dev, diff --git a/src/iop/retouch.c b/src/iop/retouch.c index 1ce4a9ddc2bc..8ffbcac1d1c4 100644 --- a/src/iop/retouch.c +++ b/src/iop/retouch.c @@ -897,12 +897,13 @@ static int rt_masks_point_calc_delta(dt_iop_module_t *self, } /* returns (dx dy) to get from the source to the destination */ -static int rt_masks_get_delta_to_destination(dt_iop_module_t *self, +static int rt_masks_get_transform_to_destination(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi, dt_masks_form_t *form, float *dx, float *dy, + float *angle, const int distort_mode) { int res = 0; @@ -914,6 +915,13 @@ static int rt_masks_get_delta_to_destination(dt_iop_module_t *self, res = rt_masks_point_calc_delta(self, piece, roi, pt->corner, form->source, dx, dy, distort_mode); } + else if(form->type & DT_MASKS_BRUSH) + { + const dt_masks_point_brush_t *pt = form->points->data; + + res = rt_masks_point_calc_delta(self, piece, roi, pt->corner, + form->source, dx, dy, distort_mode); + } else if(form->type & DT_MASKS_CIRCLE) { const dt_masks_point_circle_t *pt = form->points->data; @@ -928,13 +936,9 @@ static int rt_masks_get_delta_to_destination(dt_iop_module_t *self, res = rt_masks_point_calc_delta(self, piece, roi, pt->center, form->source, dx, dy, distort_mode); } - else if(form->type & DT_MASKS_BRUSH) - { - const dt_masks_point_brush_t *pt = form->points->data; - res = rt_masks_point_calc_delta(self, piece, roi, pt->corner, - form->source, dx, dy, distort_mode); - } + if (angle) + *angle = form->source[2]; return res; } @@ -2799,14 +2803,35 @@ static void rt_compute_roi_in(dt_iop_module_t *self, if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_HEAL || p->rt_forms[index].algorithm == DT_IOP_RETOUCH_CLONE) { - float dx = 0.f, dy = 0.f; - if(rt_masks_get_delta_to_destination(self, piece, roi_in, form, &dx, &dy, - p->rt_forms[index].distort_mode)) + float dx = 0.f, dy = 0.f, angle = 0.f; + if(rt_masks_get_transform_to_destination( + self, piece, roi_in, form, &dx, &dy, &angle, p->rt_forms[index].distort_mode)) { + // source region = destination box translated by (-dx, -dy) roiy = fminf(ft - dy, roiy); roix = fminf(fl - dx, roix); roir = fmaxf(fl + fw - dx, roir); roib = fmaxf(ft + fh - dy, roib); + + // when the source is rotated, the box above does not bound the + // rotated footprint. dt_masks_get_source_area already applies the + // source rotation (it goes through the source point generation), so + // it gives the true rotated source bbox -- union it in. + if(!feqf(angle, 0.0f, 0.01f)) + { + int sfl, sft, sfw, sfh; + if(dt_masks_get_source_area(self, piece, form, &sfw, &sfh, &sfl, &sft)) + { + sfw *= roi_in->scale; + sfh *= roi_in->scale; + sfl *= roi_in->scale; + sft *= roi_in->scale; + roiy = fminf(sft, roiy); + roix = fminf(sfl, roix); + roir = fmaxf(sfl + sfw, roir); + roib = fmaxf(sft + sfh, roib); + } + } } } } @@ -2885,7 +2910,7 @@ static void rt_extend_roi_in_from_source_clones(dt_iop_module_t *self, // get the destination area int fl_dest, ft_dest; float dx = 0.f, dy = 0.f; - if(!rt_masks_get_delta_to_destination(self, piece, roi_in, form, &dx, &dy, + if(!rt_masks_get_transform_to_destination(self, piece, roi_in, form, &dx, &dy, NULL, p->rt_forms[index].distort_mode)) { continue; @@ -3234,28 +3259,137 @@ static void rt_intersect_2_rois(dt_iop_roi_t *const roi_1, roi_dest->height = y_to - y_from; } +// opacity-weighted centroid (center of mass) of a single-channel mask, in +// roi_mask_scaled-local coords. The centroid is rotation-invariant, which is why +// it -- not the bounding-box center -- is used as the rotation pivot: it stays +// put as the source is rotated, matching the overlay which pivots about the same +// (area) centroid (see _path_centroid / _brush_centroid). +static void rt_mask_centroid(const float *const mask_scaled, + const dt_iop_roi_t *const roi_mask_scaled, + float *const cx, + float *const cy) +{ + const int w = roi_mask_scaled->width; + const int h = roi_mask_scaled->height; + double sx = 0.0, sy = 0.0, sw = 0.0; + + for(int y = 0; y < h; y++) + { + const float *const m = mask_scaled + (size_t)y * w; + for(int x = 0; x < w; x++) + { + const float v = m[x]; + sx += (double)v * x; + sy += (double)v * y; + sw += v; + } + } + + if(sw > 1e-6) + { + *cx = (float)(sx / sw); + *cy = (float)(sy / sw); + } + else + { + *cx = w * 0.5f; + *cy = h * 0.5f; + } +} + +// (cx, cy) is the rotation pivot in roi_out-local coords (the mask centroid); +// only used when angle != 0. static void rt_copy_in_to_out(const float *const in, const dt_iop_roi_t *const roi_in, float *const out, const dt_iop_roi_t *const roi_out, const int ch, const int dx, - const int dy) + const int dy, + const float angle, + const float cx, + const float cy) { const size_t rowsize = sizeof(float) * ch * MIN(roi_out->width, roi_in->width); const int xoffs = roi_out->x - roi_in->x - dx; const int yoffs = roi_out->y - roi_in->y - dy; const int y_to = MIN(roi_out->height, roi_in->height); - DT_OMP_FOR() - for(int y = 0; y < y_to; y++) + // treat a negligibly small rotation as no rotation: skips the resampling + // path (cheaper) and avoids artifacts from repeated UI actions + if(feqf(angle, 0.0f, 0.01f)) { - const size_t iindex = ((size_t)(y + yoffs) * roi_in->width + xoffs) * ch; - const size_t oindex = (size_t)y * roi_out->width * ch; - float *in1 = (float *)in + iindex; - float *out1 = (float *)out + oindex; + DT_OMP_FOR() + for(int y = 0; y < y_to; y++) + { + const size_t iindex = ((size_t)(y + yoffs) * roi_in->width + xoffs) * ch; + const size_t oindex = (size_t)y * roi_out->width * ch; + const float *in1 = in + iindex; + float *out1 = out + oindex; - memcpy(out1, in1, rowsize); + memcpy(out1, in1, rowsize); + } + } + else + { + // Sampling with rotation: the destination pixel at offset (rx,ry) from the + // mask centroid is filled with the image pixel that lies under the rotated + // source overlay at the matching offset, i.e. centroid + R(angle)*(rx,ry). + // This uses the SAME rotation and pivot as the on-screen source outline + // (masks code), so the overlay marks the region being copied. + const float c = cosf(angle); + const float s = sinf(angle); + // Rotation pivot: the mask centroid (passed in as cx, cy in roi_out-local + // coords), mapped into the source region in roi_in coordinates. + const float cx_source = cx + xoffs; + const float cy_source = cy + yoffs; + const int x_to = MIN(roi_out->width, roi_in->width); + + DT_OMP_FOR() + for(int y = 0; y < y_to; y++) + { + for(int x = 0; x < x_to; x++) + { + // Translate to source, rotate around source center, get final position + const float sx = x + xoffs; + const float sy = y + yoffs; + const float rx = sx - cx_source; + const float ry = sy - cy_source; + const float ix = cx_source + rx * c - ry * s; + const float iy = cy_source + rx * s + ry * c; + + const size_t oindex = ((size_t)y * roi_out->width + x) * ch; + float *out1 = out + oindex; + + // Edge-clamp (replicate) rather than zero-filling out-of-bounds samples: + // rt_compute_roi_in grows roi_in to the rotation-aware source area, but it + // cannot grow past the actual image border (and the box corners outside + // the mask are not covered), so a rotated source near the image edge can + // still sample just outside roi_in. Replicating the border keeps a benign + // smear there instead of seeding black pixels, which would otherwise + // create a hard seam for heal. + const float ixc = CLAMPF(ix, 0.0f, roi_in->width - 1.0f); + const float iyc = CLAMPF(iy, 0.0f, roi_in->height - 1.0f); + + const int x0 = CLAMP((int)ixc, 0, roi_in->width - 2); + const int y0 = CLAMP((int)iyc, 0, roi_in->height - 2); + const float dx0 = ixc - x0; + const float dy0 = iyc - y0; + + const float *in00 = in + ((size_t)y0 * roi_in->width + x0) * ch; + const float *in10 = in + ((size_t)y0 * roi_in->width + x0 + 1) * ch; + const float *in01 = in + ((size_t)(y0 + 1) * roi_in->width + x0) * ch; + const float *in11 = in + ((size_t)(y0 + 1) * roi_in->width + x0 + 1) * ch; + + for(int c_idx = 0; c_idx < ch; c_idx++) + { + const float val = in00[c_idx] * (1.0f - dx0) * (1.0f - dy0) + + in10[c_idx] * dx0 * (1.0f - dy0) + in01[c_idx] * (1.0f - dx0) * dy0 + + in11[c_idx] * dx0 * dy0; + out1[c_idx] = val; + } + } + } } } @@ -3416,6 +3550,9 @@ static void _retouch_clone(float *const in, dt_iop_roi_t *const roi_mask_scaled, const int dx, const int dy, + const float angle, + const float cx, + const float cy, const float opacity) { // alloc temp image to avoid issues when areas self-intersects @@ -3427,7 +3564,7 @@ static void _retouch_clone(float *const in, } // copy source image to tmp - rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy); + rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy, angle, cx, cy); // clone it rt_copy_image_masked(img_src, in, roi_in, mask_scaled, roi_mask_scaled, opacity); @@ -3460,7 +3597,7 @@ static void _retouch_blur(dt_iop_module_t *self, // copy source image so we blur just the mask area (at least the // smallest rect that covers it) - rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0); + rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0, 0.0f, 0.0f, 0.0f); if(blur_type == DT_IOP_RETOUCH_BLUR_GAUSSIAN && fabsf(blur_radius) > 0.1f) { @@ -3531,6 +3668,9 @@ static void _retouch_heal(float *const in, dt_iop_roi_t *const roi_mask_scaled, const int dx, const int dy, + const float angle, + const float cx, + const float cy, const float opacity, const int max_iter) { @@ -3544,8 +3684,8 @@ static void _retouch_heal(float *const in, } // copy source and destination to temp images - rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy); - rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0); + rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy, angle, cx, cy); + rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0, 0.0f, 0.0f, 0.0f); // heal it dt_heal(img_src, img_dest, mask_scaled, @@ -3658,11 +3798,11 @@ static void rt_process_forms(float *layer, dwt_params_t *const wt_p, const int s // search the delta with the source const dt_iop_retouch_algo_type_t algo = p->rt_forms[index].algorithm; - float dx = 0.f, dy = 0.f; + float dx = 0.f, dy = 0.f, angle = 0.f; if(algo != DT_IOP_RETOUCH_BLUR && algo != DT_IOP_RETOUCH_FILL) { - if(!rt_masks_get_delta_to_destination(self, piece, roi_layer, form, &dx, &dy, + if(!rt_masks_get_transform_to_destination(self, piece, roi_layer, form, &dx, &dy, &angle, p->rt_forms[index].distort_mode)) { dt_free_align(mask); @@ -3689,6 +3829,11 @@ static void rt_process_forms(float *layer, dwt_params_t *const wt_p, const int s continue; } + // rotation pivot: mask centroid in roi_mask_scaled-local coords + float cx = 0.f, cy = 0.f; + if(!feqf(angle, 0.0f, 0.01f)) + rt_mask_centroid(mask_scaled, &roi_mask_scaled, &cx, &cy); + if((dx != 0 || dy != 0 || algo == DT_IOP_RETOUCH_BLUR @@ -3699,12 +3844,12 @@ static void rt_process_forms(float *layer, dwt_params_t *const wt_p, const int s if(algo == DT_IOP_RETOUCH_CLONE) { _retouch_clone(layer, roi_layer, mask_scaled, - &roi_mask_scaled, dx, dy, form_opacity); + &roi_mask_scaled, dx, dy, angle, cx, cy, form_opacity); } else if(algo == DT_IOP_RETOUCH_HEAL) { _retouch_heal(layer, roi_layer, mask_scaled, - &roi_mask_scaled, dx, dy, form_opacity, p->max_heal_iter); + &roi_mask_scaled, dx, dy, angle, cx, cy, form_opacity, p->max_heal_iter); } else if(algo == DT_IOP_RETOUCH_BLUR) { @@ -3885,7 +4030,7 @@ void process(dt_iop_module_t *self, } // return final image - rt_copy_in_to_out(in_retouch, roi_rt, ovoid, roi_out, 4, 0, 0); + rt_copy_in_to_out(in_retouch, roi_rt, ovoid, roi_out, 4, 0, 0, 0.0f, 0.0f, 0.0f); cleanup: dt_free_align(in_retouch); @@ -3899,7 +4044,7 @@ void distort_mask(dt_iop_module_t *self, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { - rt_copy_in_to_out(in, roi_in, out, roi_out, 1, 0, 0); + rt_copy_in_to_out(in, roi_in, out, roi_out, 1, 0, 0, 0.0f, 0.0f, 0.0f); } #ifdef HAVE_OPENCL @@ -3984,6 +4129,9 @@ static cl_int rt_copy_in_to_out_cl(const int devid, const dt_iop_roi_t *const roi_out, const int dx, const int dy, + const float angle, + const float cx, + const float cy, const int kernel) { cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE; @@ -4002,7 +4150,7 @@ static cl_int rt_copy_in_to_out_cl(const int devid, err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, MIN(roi_out->width, roi_in->width), MIN(roi_out->height, roi_in->height), CLARG(dev_in), CLARG(dev_roi_in), CLARG(dev_out), CLARG(dev_roi_out), - CLARG(xoffs), CLARG(yoffs)); + CLARG(xoffs), CLARG(yoffs), CLARG(angle), CLARG(cx), CLARG(cy)); if(err != CL_SUCCESS) dt_print(DT_DEBUG_ALWAYS, "rt_copy_in_to_out_cl error 2"); @@ -4120,6 +4268,9 @@ static cl_int _retouch_clone_cl(const int devid, dt_iop_roi_t *const roi_mask_scaled, const int dx, const int dy, + const float angle, + const float cx, + const float cy, const float opacity, dt_iop_retouch_global_data_t *gd) { @@ -4131,7 +4282,7 @@ static cl_int _retouch_clone_cl(const int devid, goto cleanup; // copy source image to tmp - err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_src, roi_mask_scaled, dx, dy, + err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_src, roi_mask_scaled, dx, dy, angle, cx, cy, gd->kernel_retouch_copy_buffer_to_buffer); if(err != CL_SUCCESS) goto cleanup; @@ -4214,7 +4365,7 @@ static cl_int _retouch_blur_cl(const int devid, } err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_dest, - roi_mask_scaled, 0, 0, + roi_mask_scaled, 0, 0, 0.0f, 0.0f, 0.0f, gd->kernel_retouch_copy_buffer_to_image); if(err != CL_SUCCESS) goto cleanup; @@ -4279,14 +4430,17 @@ static cl_int _retouch_blur_cl(const int devid, static cl_int _retouch_heal_cl(const int devid, cl_mem dev_layer, dt_iop_roi_t *const roi_layer, - float *mask_scaled, cl_mem dev_mask_scaled, + float *const mask_scaled, dt_iop_roi_t *const roi_mask_scaled, const int dx, const int dy, + const float angle, + const float cx, + const float cy, const float opacity, - dt_iop_retouch_global_data_t *gd, - const int max_iter) + const int max_iter, + dt_iop_retouch_global_data_t *gd) { cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE; @@ -4297,13 +4451,13 @@ static cl_int _retouch_heal_cl(const int devid, goto cleanup; err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_src, - roi_mask_scaled, dx, dy, + roi_mask_scaled, dx, dy, angle, cx, cy, gd->kernel_retouch_copy_buffer_to_buffer); if(err != CL_SUCCESS) goto cleanup; err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_dest, - roi_mask_scaled, 0, 0, + roi_mask_scaled, 0, 0, 0.0f, 0.0f, 0.0f, gd->kernel_retouch_copy_buffer_to_buffer); if(err != CL_SUCCESS) goto cleanup; @@ -4440,13 +4594,13 @@ static cl_int rt_process_forms_cl(cl_mem dev_layer, continue; } - float dx = 0.f, dy = 0.f; + float dx = 0.f, dy = 0.f, angle = 0.f; // search the delta with the source const dt_iop_retouch_algo_type_t algo = p->rt_forms[index].algorithm; if(algo != DT_IOP_RETOUCH_BLUR && algo != DT_IOP_RETOUCH_FILL) { - if(!rt_masks_get_delta_to_destination(self, piece, roi_layer, form, &dx, &dy, + if(!rt_masks_get_transform_to_destination(self, piece, roi_layer, form, &dx, &dy, &angle, p->rt_forms[index].distort_mode)) { dt_free_align(mask); @@ -4466,6 +4620,12 @@ static cl_int rt_process_forms_cl(cl_mem dev_layer, &roi_mask_scaled, roi_layer, dx, dy, algo); + // rotation pivot: mask centroid in roi_mask_scaled-local coords + // (computed before mask_scaled may be freed below) + float cx = 0.f, cy = 0.f; + if(!feqf(angle, 0.0f, 0.01f) && mask_scaled != NULL) + rt_mask_centroid(mask_scaled, &roi_mask_scaled, &cx, &cy); + // only heal needs mask scaled if(algo != DT_IOP_RETOUCH_HEAL && mask_scaled != NULL) { @@ -4494,14 +4654,14 @@ static cl_int rt_process_forms_cl(cl_mem dev_layer, if(algo == DT_IOP_RETOUCH_CLONE) { err = _retouch_clone_cl(devid, dev_layer, roi_layer, - dev_mask_scaled, &roi_mask_scaled, dx, dy, + dev_mask_scaled, &roi_mask_scaled, dx, dy, angle, cx, cy, form_opacity, gd); } else if(algo == DT_IOP_RETOUCH_HEAL) { err = _retouch_heal_cl(devid, dev_layer, roi_layer, - mask_scaled, dev_mask_scaled, &roi_mask_scaled, dx, - dy, form_opacity, gd, p->max_heal_iter); + dev_mask_scaled, mask_scaled, &roi_mask_scaled, dx, + dy, angle, cx, cy, form_opacity, p->max_heal_iter, gd); } else if(algo == DT_IOP_RETOUCH_BLUR) { @@ -4711,7 +4871,7 @@ int process_cl(dt_iop_module_t *self, } // return final image - err = rt_copy_in_to_out_cl(devid, in_retouch, roi_in, dev_out, roi_out, 0, 0, + err = rt_copy_in_to_out_cl(devid, in_retouch, roi_in, dev_out, roi_out, 0, 0, 0.0f, 0.0f, 0.0f, gd->kernel_retouch_copy_buffer_to_image); cleanup: