GPv3: Merge Points #116726

Closed
Pedro wants to merge 2 commits from (deleted):gpv3_merge_points into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
First-time contributor

Create new stroke of selected points in active layer. Resolves #113582 and #113570

In terms of UI, I left the operator in the Grease Pencil menu, since it works for both point and strokes mode.

Video with demo for the points selection mode:

Demo with strokes selection mode:

Create new stroke of selected points in active layer. Resolves #113582 and #113570 In terms of UI, I left the operator in the `Grease Pencil` menu, since it works for both point and strokes mode. Video with demo for the points selection mode: <video src="/attachments/1e3b8e1d-2b76-4d8c-9502-49a5020afa25" title="points_demo.mp4" controls></video> Demo with strokes selection mode: <video src="/attachments/d6a95abb-e44b-4da1-bc69-8fc6ff440125" title="strokes_demo.mp4" controls></video>
Iliya Katushenock added this to the Grease Pencil project 2024-01-02 23:12:19 +01:00
casey-bianco-davis requested changes 2024-01-03 20:43:45 +01:00
casey-bianco-davis left a comment
Member

Here's some feedback:

Blender uses Clang Format which automatically formats code, you can run clang with use the command make format. (personally I have an add-on for my IDE that will run clang when I save files)

I noticed a lot of auto in loops and I think that it would better to be more explicit in the types.

Here's some feedback: Blender uses [Clang Format](https://wiki.blender.org/wiki/Tools/ClangFormat) which automatically formats code, you can run clang with use the command `make format`. (personally I have an add-on for my IDE that will run clang when I save files) I noticed a lot of `auto` in loops and I think that it would better to be more explicit in the types.
casey-bianco-davis requested changes 2024-01-03 22:31:21 +01:00
casey-bianco-davis left a comment
Member

Ideally it would be possible to work on the active layer. Maybe create a new CurvesGeometry at the start and keep adding geometry to it, and then add it to the active layer at the end?
although this would require working in a order (i.e. use for (const MutableDrawingInfo &info : drawings) { instead of parallel)

Ideally it would be possible to work on the active layer. Maybe create a new `CurvesGeometry` at the start and keep adding geometry to it, and then add it to the active layer at the end? although this would require working in a order (i.e. use `for (const MutableDrawingInfo &info : drawings) {` instead of parallel)
@ -1641,0 +1641,4 @@
static int grease_pencil_stroke_merge_exec(bContext *C, wmOperator * /* op */)
{
auto compute_src_ranges = [](IndexMask src_selection, const bke::CurvesGeometry &src_geometry) -> Vector<IndexRange> {
Vector<IndexRange> initial_ranges = src_selection.to_ranges();

This can be a const Span<IndexRange>

This can be a `const Span<IndexRange>`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1643,4 @@
auto compute_src_ranges = [](IndexMask src_selection, const bke::CurvesGeometry &src_geometry) -> Vector<IndexRange> {
Vector<IndexRange> initial_ranges = src_selection.to_ranges();
// Splitting the source selection by ranges doesn't take into account the strokes,

For multi line comments it better to use /* */ rather than //
i.e.

/**
  * Splitting the source selection by ranges doesn't take into account the strokes,
  * i.e, if both the end of an stroke and the beginning of the next are selected, all the
  * indices end up in the same range. Let's refine the splitting.
  */
For multi line comments it better to use `/* */` rather than `//` i.e. ``` /** * Splitting the source selection by ranges doesn't take into account the strokes, * i.e, if both the end of an stroke and the beginning of the next are selected, all the * indices end up in the same range. Let's refine the splitting. */ ```
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1648,4 @@
// indices end up in the same range. Let's refine the splitting
Vector<IndexRange> final_ranges{};
const Array<int> points_map = src_geometry.point_to_curve_map();
for (const auto index_range : initial_ranges) {

I think some name like initial_range would be more clear and use IndexRange instead of auto

I think some name like `initial_range` would be more clear and use `IndexRange` instead of `auto`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1737,4 @@
bke::greasepencil::Drawing *act_drawing = grease_pencil.get_editable_drawing_at(active_layer, current_frame);
bke::CurvesGeometry& act_geometry = act_drawing->strokes_for_write();
for (auto layer : grease_pencil.layers_for_write())

There has been some changes to the API since #113582 was written and now
threading::parallel_for_each(drawings, [&](MutableDrawingInfo &info) { should be used.
you can get the CurvesGeometry with info.drawing.strokes_for_write()
and all of the continues would need to be replaced with returns.

There has been some changes to the API since #113582 was written and now `threading::parallel_for_each(drawings, [&](MutableDrawingInfo &info) {` should be used. you can get the `CurvesGeometry` with `info.drawing.strokes_for_write()` and all of the `continue`s would need to be replaced with `return`s.
Member

#113582 is indeed outdated 😅

#113582 is indeed outdated 😅
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1743,4 @@
continue;
}
auto *drawing = grease_pencil.get_editable_drawing_at(layer, current_frame);

If you use info.drawing instead of getting the current frame, it would allow for this operator to work with multi frame editing.

If you use `info.drawing` instead of getting the current frame, it would allow for this operator to work with multi frame editing.
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1750,4 @@
}
IndexMaskMemory memory;
const bke::CurvesGeometry &geometry = drawing->strokes();

The name curves has been used for the CurvesGeometry

i.e.
bke::CurvesGeometry &curves = info.drawing.strokes_for_write();

The name `curves` has been used for the `CurvesGeometry` i.e. `bke::CurvesGeometry &curves = info.drawing.strokes_for_write();`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1751,4 @@
IndexMaskMemory memory;
const bke::CurvesGeometry &geometry = drawing->strokes();
IndexMask points_selection = curves::retrieve_selected_points(geometry, memory);

retrieve_editable_and_selected_points should be used instead of retrieve_selected_points
because grease pencil materials can be locked and become not editable.

i.e.
const IndexMask points_selection = ed::greasepencil::retrieve_editable_and_selected_points( *object, info.drawing, memory);

`retrieve_editable_and_selected_points` should be used instead of `retrieve_selected_points` because grease pencil materials can be locked and become not editable. i.e. `const IndexMask points_selection = ed::greasepencil::retrieve_editable_and_selected_points( *object, info.drawing, memory);`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1757,4 @@
continue;
}
const Vector<IndexRange> src_ranges = compute_src_ranges(points_selection, geometry);

I think this Vector can be replaced when a Span or Array

I think this `Vector` can be replaced when a `Span` or `Array`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1763,4 @@
src_ranges, points_selection.size(), act_geometry);
copy_attributes_for_selected_ranges(geometry, src_ranges, new_geometry_offsets, act_geometry);
}

For operations that create new strokes can be nice to deselect the old strokes.

some thing like this:

bke::MutableAttributeAccessor attributes = geometry.attributes_for_write();
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
        ".selection", bke::AttrDomain::Point);
index_mask::masked_fill(selection.span, false, points_selection);
selection.finish();

after the attributes are copy.
And make geometry not const

For operations that create new strokes can be nice to deselect the old strokes. some thing like this: ``` bke::MutableAttributeAccessor attributes = geometry.attributes_for_write(); bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>( ".selection", bke::AttrDomain::Point); index_mask::masked_fill(selection.span, false, points_selection); selection.finish(); ``` after the attributes are copy. And make `geometry` not `const`
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1766,4 @@
}
// WIP: this was hardcoded to prevent an old crash, requires fix
act_geometry.fill_curve_types(CURVE_TYPE_CATMULL_ROM);

I tried and could not get a crash to occur. Check if this line is still need on your computer.

I tried and could not get a crash to occur. Check if this line is still need on your computer.
Author
First-time contributor

I was experiencing crashes in the call to act_geometry.update_curve_types(); and I came out with this quick "fix". I suppose curve types shuould be copied as well before calling the update... function. At the moment I was unaware of that.

I was experiencing crashes in the call to `act_geometry.update_curve_types();` and I came out with this quick "fix". I suppose curve types shuould be copied as well before calling the `update...` function. At the moment I was unaware of that.
Member

Crash likely to happen when curve attributes are not updated.

Crash likely to happen when curve attributes are not updated.
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1771,4 @@
act_drawing->tag_topology_changed();
act_geometry.update_curve_types();
return OPERATOR_FINISHED;

It's important to tag for updates.

DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil);
It's important to tag for updates. ``` DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY); WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil); ```
casey-bianco-davis marked this conversation as resolved
Author
First-time contributor

Thanks a lot for the careful review, I'll be applying the requested changes. Also, I was not very happy with having many big lambdas within a single function, so I moved them to helper static functions within the same file. I'll be pushing the changes soon.

Thanks a lot for the careful review, I'll be applying the requested changes. Also, I was not very happy with having many big lambdas within a single function, so I moved them to helper static functions within the same file. I'll be pushing the changes soon.
Pratik Borhade reviewed 2024-01-04 11:27:32 +01:00
Pratik Borhade left a comment
Member

Hi, thanks for the PR. It's still in WIP but few points apart from Casey's detailed review :)

Hi, thanks for the PR. It's still in WIP but few points apart from Casey's detailed review :)
@ -1640,1 +1640,4 @@
static int grease_pencil_stroke_merge_exec(bContext *C, wmOperator * /* op */)
{
auto compute_src_ranges = [](IndexMask src_selection, const bke::CurvesGeometry &src_geometry) -> Vector<IndexRange> {
Member

i think source range should be "selection" range, you can use retrieve_editable_and_selected_points instead

i think source range should be "selection" range, you can use `retrieve_editable_and_selected_points` instead
casey-bianco-davis marked this conversation as resolved
@ -1641,0 +1675,4 @@
return final_ranges;
};
auto resize_and_compute_new_offsets =
Member

I don't think you require to compute offset value. since we're creating a new "stroke" out of selecting points so offset size will be 2 = (0, selected_points_count)

I don't think you require to compute offset value. since we're creating a new "stroke" out of selecting points so offset size will be 2 = (0, selected_points_count)
Author
First-time contributor

This is a good point. Initially I thought that each group of consecutive selected points should be a separate stroke in the dst curves. And if I understood the data structures correctly, each stroke is identified by two consecutive elements in the offsets array.

Most of the code I've written focuses on achieving this: if a user selects m vertices (from possibly different strokes and layers), forming a set of n consecutive ranges (some of them possibly from the same stroke), I create n new entries in the offsets array.

IDK if this is correct, overly complicated, necessary or even useful. You guys tell me. If it's enough to have all the points gathered in a single stroke (despite they may not be visually connected), that would simplify the implementation a lot.

This is a good point. Initially I thought that each group of consecutive selected points should be a separate stroke in the dst curves. And if I understood the data structures correctly, each stroke is identified by two consecutive elements in the offsets array. Most of the code I've written focuses on achieving this: if a user selects _m_ vertices (from possibly different strokes and layers), forming a set of _n_ consecutive ranges (some of them possibly from the same stroke), I create _n_ new entries in the offsets array. IDK if this is correct, overly complicated, necessary or even useful. You guys tell me. If it's enough to have all the points gathered in a single stroke (despite they may not be visually connected), that would simplify the implementation a lot.
Member

You can refer legacy GP operator for the final result. Offset length will be 2 here as said earlier :)

despite they may not be visually connected

If you make offset 2 and update curve-domain attributes properly, all selected points of new stroke will be connected.

You can refer legacy GP operator for the final result. Offset length will be 2 here as said earlier :) > despite they may not be visually connected If you make offset 2 and update curve-domain attributes properly, all selected points of new stroke will be connected.
Author
First-time contributor

Ok, looks like I missunderstood the requirements. The objective is not to copy the selections into the active layer, but to connect in a single stroke the points selected. Let me rework a bit on this and will come back with a better (and simpler) implementation

Ok, looks like I missunderstood the requirements. The objective is not to copy the selections into the active layer, but to connect in a single stroke the points selected. Let me rework a bit on this and will come back with a better (and simpler) implementation
PRiera1 marked this conversation as resolved

Also you don't need to have a reference to 113582 in the title, instead it better to use resolves #113582 in the body of the PR, this makes it so that when this PR is merge it will automatically close 113582

Also you don't need to have a reference to 113582 in the title, instead it better to use `resolves #113582` in the body of the PR, this makes it so that when this PR is merge it will automatically close 113582
Pedro reviewed 2024-01-04 23:49:31 +01:00
@ -1641,0 +1773,4 @@
bool changed = false;
threading::parallel_for_each(drawings, [&](MutableDrawingInfo &info) {
IndexMaskMemory memory;
Author
First-time contributor

Do we need a memory for each call to the lambda, or could it be declared outside and be captured? (I'm worried about possible race conditions)

Do we need a memory for each call to the lambda, or could it be declared outside and be captured? (I'm worried about possible race conditions)

Each thread only needs to access the IndexMask that was create in the it's own thread, the memory only needs to live for the length of the thread and so the memory should be created with in the lambda.

Each thread only needs to access the `IndexMask` that was create in the it's own thread, the memory only needs to live for the length of the thread and so the memory should be created with in the lambda.
PRiera1 marked this conversation as resolved
Pedro changed title from WIP: #113582 gpv3_merge_points to WIP: gpv3_merge_points 2024-01-05 00:15:50 +01:00
Author
First-time contributor

Just pushed some new commits, which address most (if not all) the points above. As mentioned earlier I've also changed a bit the code structure. I'm copying now the curve types and with that the crash has disappeared.

Just pushed some new commits, which address most (if not all) the points above. As mentioned earlier I've also changed a bit the code structure. I'm copying now the curve types and with that the crash has disappeared.
Falk David changed title from WIP: gpv3_merge_points to WIP: GPv3: Merge Points 2024-01-08 14:04:00 +01:00
Falk David requested review from Falk David 2024-01-08 14:04:11 +01:00
Pedro force-pushed gpv3_merge_points from 7aa1195b9e to 3f7b34b400 2024-01-12 21:55:22 +01:00 Compare
Author
First-time contributor

Hi, just pushed new commits with this time a proper implementation of the feature. I also updated the demo video to show the new behavior. There are some cases to fix yet, but I think changes go now in the proper direction.

Hi, just pushed new commits with this time a proper implementation of the feature. I also updated the demo video to show the new behavior. There are some cases to fix yet, but I think changes go now in the proper direction.
Author
First-time contributor

Hope the implementation looks good now. I have some questions/comments anyway and I hope they'll help to improve the solution:

  • I'll add some comments to the new functions/structs explaining their purpose. I expect to push that commit during today.
  • Does it makes sense to create some unit tests for this? I haven't checked the tests in the project yet, but I think this feature is complex enough to make sense having some tests covering it.
  • Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI.
  • I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further and create a small delegate class for this (GreasePencilPointMerger, or something like that) It would have a single public method receiving the pencil as a parameter. If you agree with this approach, were should I put that class?
  • I think it makes sense to add this method to the GreasePencil struct: std::optional<int> get_layer_index(const *Layer) const This way the code computing the active_layer_index wouldn't be necessary
Hope the implementation looks good now. I have some questions/comments anyway and I hope they'll help to improve the solution: - I'll add some comments to the new functions/structs explaining their purpose. I expect to push that commit during today. - Does it makes sense to create some unit tests for this? I haven't checked the tests in the project yet, but I think this feature is complex enough to make sense having some tests covering it. - Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI. - I created the namespace `stroke_merge_utils` to keep my changes isolated, but I think is worth going a step further and create a small delegate class for this (`GreasePencilPointMerger`, or something like that) It would have a single public method receiving the pencil as a parameter. If you agree with this approach, were should I put that class? - I think it makes sense to add this method to the `GreasePencil` struct: `std::optional<int> get_layer_index(const *Layer) const` This way the code computing the `active_layer_index` wouldn't be necessary
Falk David requested changes 2024-01-15 15:56:06 +01:00
Falk David left a comment
Member

Did a pass on this. Generally, I'd like to get more input from e.g. @HooglyBoogly on the algorithm.

Did a pass on this. Generally, I'd like to get more input from e.g. @HooglyBoogly on the algorithm.
@ -1040,2 +1039,4 @@
this->curve_num = curves_num;
}
this->curve_offsets[this->curve_num] = this->point_num;
Member

@HooglyBoogly Do you agree with this change?

@HooglyBoogly Do you agree with this change?
Member

I don't think this should be changed. Resizing the points should usually require rebuilding the offsets anyway. Or if points are just added to the last curve, that should be explicit.

I don't think this should be changed. Resizing the points should usually require rebuilding the offsets anyway. Or if points are just added to the last curve, that should be explicit.
Author
First-time contributor

I suppose you mean calling offsets_for_write and setting the last offset explicitly. I'll take that into account and remove this change. Anyway, I'm thinking now about resizing the geometry only once at the beginning, so this change won't be necessary actually

I suppose you mean calling `offsets_for_write` and setting the last offset explicitly. I'll take that into account and remove this change. Anyway, I'm thinking now about resizing the geometry only once at the beginning, so this change won't be necessary actually
PRiera1 marked this conversation as resolved
@ -1668,1 +1670,4 @@
namespace stroke_merge_utils {
struct CurveRange {
IndexMask range_mask;
Member

Why is this not an IndexRange?

Why is this not an `IndexRange`?
Author
First-time contributor

my bad. Fixed

my bad. Fixed
PRiera1 marked this conversation as resolved
@ -1669,0 +1681,4 @@
bool is_active_layer;
};
static Array<CurveRange> compute_stroke_ranges(IndexMask selection,
Member

Use const IndexMask &selection

Use `const IndexMask &selection`
PRiera1 marked this conversation as resolved
@ -1669,0 +1725,4 @@
return final_ranges.as_span();
}
static Array<LayerSelectionInfo> obtain_selections_by_layer(
Member

-> retrieve_selections_by_layer. "retrieve" is what's used elsewhere, so better to make this consistent.

-> `retrieve_selections_by_layer`. "retrieve" is what's used elsewhere, so better to make this consistent.
PRiera1 marked this conversation as resolved
@ -1669,0 +1732,4 @@
IndexMaskMemory &memory)
{
Vector<LayerSelectionInfo> selections_by_layer;
for (MutableDrawingInfo info : frame_drawings) {
Member

Use MutableDrawingInfo & to avoid a copy

Use `MutableDrawingInfo &` to avoid a copy
PRiera1 marked this conversation as resolved
@ -1669,0 +1777,4 @@
IndexMaskMemory memory;
Array<int> mask_values(info.selected_ranges.size());
for (const auto index : mask_values.index_range()) {
Member

use const int index

use `const int index`
PRiera1 marked this conversation as resolved
@ -1669,0 +1778,4 @@
IndexMaskMemory memory;
Array<int> mask_values(info.selected_ranges.size());
for (const auto index : mask_values.index_range()) {
mask_values[index] = index * 2;
Member

This line is a bit confusing. Please add a comment that explains why the index is doubled.

This line is a bit confusing. Please add a comment that explains why the `index` is doubled.
PRiera1 marked this conversation as resolved
@ -1669,0 +1792,4 @@
dst_curves.attributes_for_write());
}
static void remove_selection(bke::CurvesGeometry &curves, IndexMask points_selection)
Member

Use const IndexMask &

Use `const IndexMask &`
PRiera1 marked this conversation as resolved
@ -1669,0 +1846,4 @@
Map<int, Vector<MutableDrawingInfo>> drawings_per_frame{};
for (auto &drawing : drawings) {
auto &frame_drawings = drawings_per_frame.lookup_or_add_default(drawing.frame_number);
Member

Use Vector<MutableDrawingInfo> &

Use `Vector<MutableDrawingInfo> &`
PRiera1 marked this conversation as resolved
@ -1669,0 +1861,4 @@
}
Span<const Layer *> layers = grease_pencil.layers();
const auto it = std::find(layers.begin(), layers.end(), active_layer);
Member

Use layers.find(active_layer) and assert it's not -1. Also remove the #include <algorithm>

Use `layers.find(active_layer)` and assert it's not `-1`. Also remove the `#include <algorithm>`
PRiera1 marked this conversation as resolved
Hans Goudey reviewed 2024-01-15 16:17:40 +01:00
Hans Goudey left a comment
Member

Didn't fully understand the algorithm yet, just hoping to get some style things out of the way first.

Didn't fully understand the algorithm yet, just hoping to get some style things out of the way first.
@ -1669,0 +1671,4 @@
namespace stroke_merge_utils {
struct CurveRange {
IndexMask range_mask;
CurveType curve_type;
Member

It seems arbitrary to just store the curve type here. What about all the other curve attributes? Those should probably be propagated all at once.

It seems arbitrary to just store the curve type here. What about all the other curve attributes? Those should probably be propagated all at once.
Author
First-time contributor

I copy only the curve types because in earlier versions I was not doing it, and there were some crashes. I figured out I had to do this in order to prevent them.

In short, I'm a first-time-contributor 😅

I'll copy the other attributes as well. But I'm not sure from which curve I should pick them. Now I'm using the ones from the first selected stroke, but perhaps would make more sense to get the ones from the active layer. Please let me know what option you prefer

I copy only the curve types because in earlier versions I was not doing it, and there were some crashes. I figured out I had to do this in order to prevent them. In short, I'm a first-time-contributor 😅 I'll copy the other attributes as well. But I'm not sure from which curve I should pick them. Now I'm using the ones from the first selected stroke, but perhaps would make more sense to get the ones from the active layer. Please let me know what option you prefer
PRiera1 marked this conversation as resolved
@ -1669,0 +1684,4 @@
static Array<CurveRange> compute_stroke_ranges(IndexMask selection,
const bke::CurvesGeometry &curves)
{
const Span<IndexRange> initial_ranges = selection.to_ranges();
Member

Span -> Vector

`Span` -> `Vector`
Author
First-time contributor

True, and I'm pretty astonished this ever worked. This is clearly UB

True, and I'm pretty astonished this ever worked. This is clearly UB
PRiera1 marked this conversation as resolved
@ -1669,0 +1692,4 @@
* indices end up in the same range. Let's refine the splitting
*/
const VArray<int8_t> curve_types = curves.curve_types();
Vector<CurveRange> final_ranges{};
Member

final_ranges{}; -> final_ranges;

`final_ranges{};` -> `final_ranges;`
PRiera1 marked this conversation as resolved
@ -1669,0 +1693,4 @@
*/
const VArray<int8_t> curve_types = curves.curve_types();
Vector<CurveRange> final_ranges{};
const Array<int> points_map = curves.point_to_curve_map();
Member

points_map -> point_to_curve_map

`points_map` -> `point_to_curve_map`
PRiera1 marked this conversation as resolved
@ -1669,0 +1722,4 @@
final_ranges.append({range, static_cast<CurveType>(curve_types[previous_curve])});
}
return final_ranges.as_span();
Member

Here you're invoking the Span constructor of Array. Better to just return the vector to avoid a copy

Here you're invoking the `Span` constructor of `Array`. Better to just return the vector to avoid a copy
PRiera1 marked this conversation as resolved
@ -1669,0 +1757,4 @@
const int dst_initial_points = dst_curves.points_num();
int previos_offset = 0;
Vector dst_raw_offsets{dst_initial_points};
Vector<int> src_raw_offsets{};
Member

Since you know the number of elements up front, use Array instead of Vector and fill in the elements by index

Since you know the number of elements up front, use `Array` instead of `Vector` and fill in the elements by index
PRiera1 marked this conversation as resolved
@ -1669,0 +1803,4 @@
static void merge_curve_attributes(const LayerSelectionInfo &info, bke::CurvesGeometry &dst_curves)
{
/**
Member

Comments inside of functions don't get the doxygen style, they would look more like this:

/* Setting to the new stroke the curve attributes and curve type of the first selected stroke
 * It's an arbitrary decission, since the original selection may encompass several strokes. */
Comments inside of functions don't get the doxygen style, they would look more like this: ``` /* Setting to the new stroke the curve attributes and curve type of the first selected stroke * It's an arbitrary decission, since the original selection may encompass several strokes. */
PRiera1 marked this conversation as resolved
@ -1669,0 +1844,4 @@
bool changed = false;
Map<int, Vector<MutableDrawingInfo>> drawings_per_frame{};
Member

Remove {} here, Map has a default constructor

Remove `{}` here, `Map` has a default constructor
PRiera1 marked this conversation as resolved
@ -1669,0 +1866,4 @@
const int active_layer_index = it - layers.begin();
IndexMaskMemory memory;
Array<LayerSelectionInfo> selections_by_layer = obtain_selections_by_layer(
Member

I would expect this code to deal with only one drawing (CurvesGeometry) at a time, so it would find the selection information and then immediately use it, rather than finding the information for all layers first

I would expect this code to deal with only one drawing (CurvesGeometry) at a time, so it would find the selection information and then immediately use it, rather than finding the information for all layers first
Author
First-time contributor

Yeah, that was my initial idea too. The reason behind this approach is the check that comes just after this call: if (selected_ranges <= 1) return
If there's only one range of selected points, there's actually nothing to merge. But in order to know that first I have to iterate through all the layers and count the selected ranges.

I could remove that check, but as a side effect, if there's only one range selected in some other layer, those points would be copied into the active layer. I thought users would see this as a bug/artifact, since nothing had to be merged but new strokes appeared anyway into the active layer.

Yeah, that was my initial idea too. The reason behind this approach is the check that comes just after this call: `if (selected_ranges <= 1) return` If there's only one range of selected points, there's actually nothing to merge. But in order to know that first I have to iterate through all the layers and count the selected ranges. I could remove that check, but as a side effect, if there's only one range selected in some other layer, those points would be copied into the active layer. I thought users would see this as a bug/artifact, since nothing had to be merged but new strokes appeared anyway into the active layer.
Member

Okay, I guess I don't fully understand what the operator is doing. I wasn't familiar with the V2 version, and I didn't imagine that it had interactions between layers.

Okay, I guess I don't fully understand what the operator is doing. I wasn't familiar with the V2 version, and I didn't imagine that it had interactions between layers.
Author
First-time contributor

Well, actually the v2 does exactly this and copies the points into the active layer in the scenario I described.

I put that check because I thought is an improvement from user's PoV. But if you're fine with the current behavior I have no problem in removing it

Well, actually the v2 does exactly this and copies the points into the active layer in the scenario I described. I put that check because I thought is an improvement from user's PoV. But if you're fine with the current behavior I have no problem in removing it
Member

I think the general idea for this transition is "when in doubt, keep the existing behavior". So sounds good

I think the general idea for this transition is "when in doubt, keep the existing behavior". So sounds good
PRiera1 marked this conversation as resolved
Member
  • Does it makes sense to create some unit tests for this? I haven't checked the tests in the project yet, but I think this feature is complex enough to make sense having some tests covering it.

Generally, we only test lower level functions that are used in the operators. And operators should not have to implement lower-level functions themselves.

  • Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI.

Yes there needs to be some change here. I think we should rename the operator as well. "Merge" is very confusing for what it actually does. @mendio What do you think?

  • I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further and create a small delegate class for this (GreasePencilPointMerger, or something like that) It would have a single public method receiving the pencil as a parameter. If you agree with this approach, were should I put that class?

No, a class is not needed.

  • I think it makes sense to add this method to the GreasePencil struct: std::optional<int> get_layer_index(const *Layer) const This way the code computing the active_layer_index wouldn't be necessary

Yes, this is needed in other places as well. I have commit ready for it.

> - Does it makes sense to create some unit tests for this? I haven't checked the tests in the project yet, but I think this feature is complex enough to make sense having some tests covering it. Generally, we only test lower level functions that are used in the operators. And operators should not have to implement lower-level functions themselves. > - Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI. Yes there needs to be some change here. I think we should rename the operator as well. "Merge" is very confusing for what it actually does. @mendio What do you think? > - I created the namespace `stroke_merge_utils` to keep my changes isolated, but I think is worth going a step further and create a small delegate class for this (`GreasePencilPointMerger`, or something like that) It would have a single public method receiving the pencil as a parameter. If you agree with this approach, were should I put that class? No, a class is not needed. > - I think it makes sense to add this method to the `GreasePencil` struct: `std::optional<int> get_layer_index(const *Layer) const` This way the code computing the `active_layer_index` wouldn't be necessary Yes, this is needed in other places as well. I have commit ready for it.
Member

Does it makes sense to create some unit tests for this?

I'd say unit tests for this would be great! Sadly we don't have the framework for adding them. We have some python operators tests, and geometry nodes tests, but no way to compare two grease pencil data-blocks for tests yet. And the geometry module doesn't have a compiled unit tests binary yet. It probably should!

I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further

IMO if you want to go further to isolate this code (which sounds like a good idea generally), I'd move it to a new file. But yeah, I agree with Falk-- making such code object-oriented usually isn't the best approach.

>Does it makes sense to create some unit tests for this? I'd say unit tests for this would be great! Sadly we don't have the framework for adding them. We have some python operators tests, and geometry nodes tests, but no way to compare two grease pencil data-blocks for tests yet. And the geometry module doesn't have a compiled unit tests binary yet. It probably should! >I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further IMO if you want to go further to isolate this code (which sounds like a good idea generally), I'd move it to a new file. But yeah, I agree with Falk-- making such code object-oriented usually isn't the best approach.
Member

I pushed 35e8959d77 :)

I pushed 35e8959d77 :)
Pedro force-pushed gpv3_merge_points from 3bed4bea05 to fb928d4d08 2024-01-15 19:18:33 +01:00 Compare
Author
First-time contributor

I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further

I'll create a new file grease_pencil_merge_utils.cc, or smth like that, in order to do not pollute this one with utility functions and keep it to the point. I won't create the class.

On other topics, I'll put comments explaining the algorithm and some of the trickiest parts of the code. Also will address the remaining issues you mentioned.

> I created the namespace stroke_merge_utils to keep my changes isolated, but I think is worth going a step further I'll create a new file `grease_pencil_merge_utils.cc`, or smth like that, in order to do not pollute this one with utility functions and keep it to the point. I won't create the class. On other topics, I'll put comments explaining the algorithm and some of the trickiest parts of the code. Also will address the remaining issues you mentioned.
Member

Sounds good.

I'll create a new file grease_pencil_merge_utils.cc, or smth like that, in order to do not pollute this one with utility functions and keep it to the point. I won't create the class.

I think you can put the entire definition of the operator in this new file, and call it something like grease_pencil_merge.cc.

Sounds good. >I'll create a new file grease_pencil_merge_utils.cc, or smth like that, in order to do not pollute this one with utility functions and keep it to the point. I won't create the class. I think you can put the entire definition of the operator in this new file, and call it something like `grease_pencil_merge.cc`.
  • Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI.

Yes there needs to be some change here. I think we should rename the operator as well. "Merge" is very confusing for what it actually does. @mendio What do you think?

The Merge operator in GPv2 never worked as desired due to the lack of the Active Point concept. With Active Point we can have all the different options that meshes have (at center, at cursor, collapse, at first, at last, by distance)
Anyway, even though this opertaor is not a proper Merge, watching the video, the operator seems to work better than in GPv2.
Maybe we could rename the operator to something like "Create Strokes from Points" and wait for Active Point to do a new real Merge Operator

> > > - Checking the legacy pencil, I saw that this option appears in a different menu. I wouldn't like to break UX, so please let me know how you prefer this operator to be called from the UI. > > Yes there needs to be some change here. I think we should rename the operator as well. "Merge" is very confusing for what it actually does. @mendio What do you think? > The Merge operator in GPv2 never worked as desired due to the lack of the Active Point concept. With Active Point we can have all the different options that meshes have (at center, at cursor, collapse, at first, at last, by distance) Anyway, even though this opertaor is not a proper Merge, watching the video, the operator seems to work better than in GPv2. Maybe we could rename the operator to something like "Create Strokes from Points" and wait for Active Point to do a new real Merge Operator
Pedro force-pushed gpv3_merge_points from 605c56bd55 to e90eaa910e 2024-01-17 12:19:49 +01:00 Compare
Pedro changed title from WIP: GPv3: Merge Points to GPv3: Merge Points 2024-01-17 12:29:00 +01:00
Author
First-time contributor

Hi, just pushed new changes which address everything pending. I also made it to work with stroke selection mode, please check the video in the PR's description.

In terms of UI, since this appears to be kind of a new operator (and there's an actual merge pending, as @mendio commented), I left the option in the Grease Pencil menu. As you can see in the video it works in both selection modes.

Also removed the WIP tag, since I think this has now a proper shape.

Hi, just pushed new changes which address everything pending. I also made it to work with stroke selection mode, please check the video in the PR's description. In terms of UI, since this appears to be kind of a new operator (and there's an actual merge pending, as @mendio commented), I left the option in the `Grease Pencil` menu. As you can see in the video it works in both selection modes. Also removed the WIP tag, since I think this has now a proper shape.
Pedro requested review from casey-bianco-davis 2024-01-17 12:36:15 +01:00
Pedro requested review from Falk David 2024-01-17 12:36:26 +01:00

This operator in stroke selection mode does something similar to what Stroke Join operator does. The Join operator algorithm in GPv2 also takes into account the distance of the ends of the strokes when joining (not just the start and end of the stroke), which provides a more accurate stroke result for artists.
In my opinion, we should avoid duplicating functionalities. I sugest to keep this operator only for point selection. What do you think @filedescriptor @antoniov ?

This operator in stroke selection mode does something similar to what Stroke Join operator does. The Join operator algorithm in GPv2 also takes into account the distance of the ends of the strokes when joining (not just the start and end of the stroke), which provides a more accurate stroke result for artists. In my opinion, we should avoid duplicating functionalities. I sugest to keep this operator only for point selection. What do you think @filedescriptor @antoniov ?

I don't think is a problem to have join at stroke and point mode, because I guess the code changes will be minimal and surely reuse almost all code (maybe is the same operator). Anyway, If to have both options is to have 2 operators, then I agree to have only one, but I don't think this is the case here.

I agree that calculate the distance between strokes is very important, not just join start or end.

EDIT: Matias, I misunderstood you...yes Join and Merge are almost the same, so better to have only one with the best of both operators.

I don't think is a problem to have join at stroke and point mode, because I guess the code changes will be minimal and surely reuse almost all code (maybe is the same operator). Anyway, If to have both options is to have 2 operators, then I agree to have only one, but I don't think this is the case here. I agree that calculate the distance between strokes is very important, not just join start or end. EDIT: Matias, I misunderstood you...yes Join and Merge are almost the same, so better to have only one with the best of both operators.

In that case my proposal is to only keep Join Operator but giving the possibility to work with points and strokes selection (what this PR code already does) The only thing we have to update in the code is the GPv2 algortihm used for the Join operator that takes into account the proximity of the stroke ends when joining (to avoid overlaping strokes unions).
This PR should become Join Operator and we only add the a new Merge operator when we have the Active Point ready.

In that case my proposal is to only keep Join Operator but giving the possibility to work with points and strokes selection (what this PR code already does) The only thing we have to update in the code is the GPv2 algortihm used for the Join operator that takes into account the proximity of the stroke ends when joining (to avoid overlaping strokes unions). This PR should become Join Operator and we only add the a new Merge operator when we have the Active Point ready.
Author
First-time contributor

I see these changes are creating some healthy discussion, so probably that means you're happy with them :)

Understood, I'll take a look at the join operator in v2, and apply the required changes so this one can become the proper join operator of v3. Give me some time so I can figure the changes out.

I see these changes are creating some healthy discussion, so probably that means you're happy with them :) Understood, I'll take a look at the join operator in v2, and apply the required changes so this one can become the proper join operator of v3. Give me some time so I can figure the changes out.

Thanks @PRiera1 , but please coordinate this with @filedescriptor because there is an ongoing PR for Join operator #113570: GPv3: Join Strokes

Thanks @PRiera1 , but please coordinate this with @filedescriptor because there is an ongoing PR for Join operator [#113570: GPv3: Join Strokes](https://projects.blender.org/blender/blender/issues/113570)
Pratik Borhade requested changes 2024-01-23 13:03:09 +01:00
Pratik Borhade left a comment
Member

Hi, checking this PR, got assert when operation is executed.

Hi, checking this PR, got assert when operation is executed.
@ -1671,0 +1677,4 @@
static void GREASE_PENCIL_OT_stroke_from_selection(wmOperatorType *ot)
{
ot->name = "Create Stroke";
ot->idname = "GREASE_PENCIL_OT_stroke_from_points";
Member

Not sure what the convention is but AFAIK idname should match with function name?

Not sure what the convention is but AFAIK `idname` should match with function name?
PRiera1 marked this conversation as resolved
@ -0,0 +232,4 @@
bool changed = false;
Map<int, Vector<MutableDrawingInfo>> drawings_per_frame;
Member

do we really need frame-drawing map? cc @filedescriptor
I'd just iterate over editable_drawings and join selected points to create a new stroke in active layer

do we really need frame-drawing map? cc @filedescriptor I'd just iterate over editable_drawings and join selected points to create a new stroke in active layer
Author
First-time contributor

At the beginning I understood that each frame has its separate drawing. But I haven't tested with animations. If different frames point to the same object then this map makes no sense, agree

At the beginning I understood that each frame has its separate drawing. But I haven't tested with animations. If different frames point to the same object then this map makes no sense, agree
Author
First-time contributor

Yeah, the map is not needed. I'll fix it

Yeah, the map is not needed. I'll fix it
Author
First-time contributor

I pushed the changes yesterday and there's no map anymore. I suppose this resolves this point and the next

I pushed the changes yesterday and there's no map anymore. I suppose this resolves this point and the next
Member

Thanks. I'll check shortly 🙂

Thanks. I'll check shortly 🙂
PRiera1 marked this conversation as resolved
@ -0,0 +241,4 @@
const Layer *active_layer = grease_pencil.get_active_layer();
if (active_layer == nullptr) {
return OPERATOR_FINISHED;
Member

OPEATOR_CANCELLED

`OPEATOR_CANCELLED`
PRiera1 marked this conversation as resolved
@ -0,0 +269,4 @@
}
const int dst_initial_points_num = dst_curves.points_num();
dst_curves.resize(dst_initial_points_num + selected_points, dst_curves.curves_num() + 1);
Member

looks like this will resize each drawing of active layer (which seems wrong to me)
i.e. if you have 3 keyframes on active layer, this will resize all 3 curves.

looks like this will resize each drawing of active layer (which seems wrong to me) i.e. if you have 3 keyframes on active layer, this will resize all 3 curves.
PRiera1 marked this conversation as resolved
Pedro force-pushed gpv3_merge_points from c57cf3b209 to 9a8254fd5f 2024-01-28 21:34:33 +01:00 Compare
Author
First-time contributor

Been working on this recently. Yesterday I updated the demo videos and IMO the strokes are connected in a more natural (and convenient) way now. As you can see, I added some functions to CurvesGeometry (I think these additions make sense)

Points pending:

  • There's still a glitch I find from time to time that the resulting stroke is cyclic for some reason, I'll try to fix that. (fixed)
  • The conversation above about whether to use a map or not (done)
  • The core of the algorithm (copy_point_attributes) is roughly O(n2/2), being n the number of selected ranges. As @filedescriptor suggested I can try to use the kd-tree to optimize it (on it)
Been working on this recently. Yesterday I updated the demo videos and IMO the strokes are connected in a more natural (and convenient) way now. As you can see, I added some functions to `CurvesGeometry` (I think these additions make sense) Points pending: - ~~There's still a glitch I find from time to time that the resulting stroke is cyclic for some reason, I'll try to fix that.~~ (fixed) - ~~The conversation above about whether to use a map or not~~ (done) - The core of the algorithm (`copy_point_attributes`) is roughly O(_n_<sup>2</sup>/2), being _n_ the number of selected ranges. As @filedescriptor suggested I can try to use the kd-tree to optimize it (on it)
Falk David requested review from Hans Goudey 2024-01-31 11:13:33 +01:00
Falk David requested changes 2024-01-31 11:51:10 +01:00
Falk David left a comment
Member

Did a pass on this. I'm not too sure about the implementation and use of struct PointsRange. But generally seems fine.

And like I mentioned before, I think compute_closest_range_to could be a lookup into a kd-tree.

Did a pass on this. I'm not too sure about the implementation and use of `struct PointsRange`. But generally seems fine. And like I mentioned before, I think `compute_closest_range_to` could be a lookup into a kd-tree.
@ -386,0 +387,4 @@
* Appends the passed curves to this one, by making a copy of its data.
* The new strokes are appended after the existing ones
*/
void append(const CurvesGeometry &other);
Member

Generally, the API for CurvesGeometry should be kept small. I think this function is not needed here and can just be a static function in grease_pencil_join_selection.
I would also add that append duplicates some behavior of geometry::join_geometries.

Generally, the API for `CurvesGeometry` should be kept small. I think this function is not needed here and can just be a static function in `grease_pencil_join_selection`. I would also add that `append` duplicates some behavior of `geometry::join_geometries`.
@ -392,0 +399,4 @@
* Change the direction of selected points (switch the start and end) without changing their
* shape.
*/
void reverse_points(const IndexMask &points_to_reverse);
Member

Same as above, this can be a static function instead.

Same as above, this can be a static function instead.
Author
First-time contributor

Well, in the same class there is already this method: void reverse_curves(const IndexMask &curves_to_reverse); I thought that, for the sake of completeness, would make sense to implement this as a member function. I can make this static anyway, but I think is worth reconsidering this point

Well, in the same class there is already this method: `void reverse_curves(const IndexMask &curves_to_reverse);` I thought that, for the sake of completeness, would make sense to implement this as a member function. I can make this static anyway, but I think is worth reconsidering this point
Member

@HooglyBoogly Would have to decide

@HooglyBoogly Would have to decide

void reverse_points(const IndexMask &points_to_reverse); is completely wrong thing:

  • IndexMask contains point indices with gaps. You cannot just reverse all data in {first, size} indices.
  • You cannot reverse points in the same mask segment, if this points is form different curves. This points is not topology-related at this point.
  • This is not const-cost function, and not trivial function (like reverse_curves which one is just change attribute of curve direction in convition way).
  • I just don't understand why points need to be reversed? Curve can be reversed. Its points can be reordered in random way. But this is not something correct in this task i guess.
`void reverse_points(const IndexMask &points_to_reverse);` is completely wrong thing: - `IndexMask` contains point indices with gaps. You cannot just reverse all data in {first, size} indices. - You cannot reverse points in the same mask segment, if this points is form different curves. This points is not topology-related at this point. - This is not const-cost function, and not trivial function (like `reverse_curves` which one is just change attribute of curve direction in convition way). - I just don't understand why points need to be reversed? Curve can be reversed. Its points can be reordered in random way. But this is not something correct in this task i guess.
@ -1422,0 +1423,4 @@
/** \name Stroke From Points Operator
* \{ */
extern int(grease_pencil_join_selection_exec)(bContext *C, wmOperator *op);
Member

Instead, define GREASE_PENCIL_OT_join_selection in grease_pencil_join_selection.cc and declare it in ED_grease_pencil.hh. Then you can still pass it to WM_operatortype_append in this file.

Instead, define `GREASE_PENCIL_OT_join_selection` in `grease_pencil_join_selection.cc` and declare it in `ED_grease_pencil.hh`. Then you can still pass it to `WM_operatortype_append` in this file.
Author
First-time contributor

Hopefully now is better

Hopefully now is better
@ -0,0 +23,4 @@
namespace blender::ed::greasepencil {
namespace {
Member

Can be removed.

Can be removed.
Author
First-time contributor

I know you guys have your style rules, and is not that I don't want to follow them, but let me justify why I used this: anonymous namespaces hide in the resulting object file not only the functions inside (same as static keyword) but also the types declared in there (which cannot be achieved solely using static)
That being said, I'll remove the namespace and mark the internal functions static, is just that I wanted to justify this point

I know you guys have your style rules, and is not that I don't want to follow them, but let me justify why I used this: anonymous namespaces hide in the resulting object file not only the functions inside (same as `static` keyword) but also the types declared in there (which cannot be achieved solely using `static`) That being said, I'll remove the namespace and mark the internal functions static, is just that I wanted to justify this point
Member

I understand. I haven't seen this much elsewhere so I thought I'd note it. You can keep it if you want.

I understand. I haven't seen this much elsewhere so I thought I'd note it. You can keep it if you want.
Member

The internal functions should be static, that's mentioned by the style guide. There's no reason to do it differently just for this file.

The internal functions should be static, that's mentioned by the style guide. There's no reason to do it differently just for this file.
Author
First-time contributor

From the style guide itself about this point:

Note that this is not a hard rule, but rather a preference.

I agree with the point in the guide about scrolling. But in this particular case, given I wanted to keep hidden the new types too, I preferred the usage of anonymous namespaces over static since the latter was not sufficient for the purpose.

From the [style guide](https://developer.blender.org/docs/handbook/guidelines/c_cpp/#anonymous-namespace) itself about this point: > Note that this is not a hard rule, but rather a preference. I agree with the point in the guide about scrolling. But in this particular case, given I wanted to keep hidden the new types too, I preferred the usage of anonymous namespaces over `static` since the latter was not sufficient for the purpose.
@ -0,0 +35,4 @@
bool belongs_to_active_layer;
};
enum class ACTION_ON_NEXT_RANGE { NOTHING, REVERSE_EXISTING, REVERSE_ADDITION, REVERSE_BOTH };
Member

The name for the enum class should be camel case: ActionOnNextRange

The name for the `enum class` should be camel case: `ActionOnNextRange`
PRiera1 marked this conversation as resolved
@ -0,0 +44,4 @@
*
* A range of points is defined as a group contiguous and visually connected points
*/
std::pair<Vector<PointsRange>, int64_t> retrieve_selection_ranges(
Member

I'd prefer if this returned just the Vector and wrote to an argument int &r_total_points_selected.

I'd prefer if this returned just the `Vector` and wrote to an argument `int &r_total_points_selected`.
PRiera1 marked this conversation as resolved
@ -0,0 +46,4 @@
*/
std::pair<Vector<PointsRange>, int64_t> retrieve_selection_ranges(
Object &object,
Span<MutableDrawingInfo> drawings,
Member

Can be const

Can be `const`
PRiera1 marked this conversation as resolved
@ -0,0 +104,4 @@
}
void apply_action(ACTION_ON_NEXT_RANGE action,
IndexRange current_range,
Member

Use const

Use `const`
PRiera1 marked this conversation as resolved
@ -0,0 +105,4 @@
void apply_action(ACTION_ON_NEXT_RANGE action,
IndexRange current_range,
IndexRange adding_range,
Member

Use const

Use `const`
PRiera1 marked this conversation as resolved
@ -0,0 +138,4 @@
* - first: the index of the closest range
* - second: the action to take
*/
std::pair<int64_t, ACTION_ON_NEXT_RANGE> compute_closest_range_to(PointsRange &range,
Member

Same as further above: I think this should return an int and write to an argument ActionOnNextRange &r_action

Same as further above: I think this should return an int and write to an argument `ActionOnNextRange &r_action`
PRiera1 marked this conversation as resolved
@ -0,0 +156,4 @@
int64_t ret_value = starting_from;
ACTION_ON_NEXT_RANGE action = ACTION_ON_NEXT_RANGE::NOTHING;
for (int64_t i = starting_from; i < ranges.size(); i++) {
Member

Could use for (const int64_t i : IndexRange(starting_from, ranges.size())) {

Could use `for (const int64_t i : IndexRange(starting_from, ranges.size())) {`

I think for (const int64_t i : IndexRange(starting_from, ranges.size())) { would not work because the second parameter of IndexRange is the size not the stop index. The correct way would be for (const int64_t i : ranges.range().drop_from(starting_from)) {

I think `for (const int64_t i : IndexRange(starting_from, ranges.size())) {` would not work because the second parameter of `IndexRange` is the size not the stop index. The correct way would be `for (const int64_t i : ranges.range().drop_from(starting_from)) {`
PRiera1 marked this conversation as resolved
@ -0,0 +159,4 @@
for (int64_t i = starting_from; i < ranges.size(); i++) {
const auto [range_begin, range_end] = get_range_begin_end(ranges[i]);
float dist = len_squared_v3v3(cur_range_end, range_begin);
Member

Use math::distance_squared

Use `math::distance_squared`
PRiera1 marked this conversation as resolved
@ -0,0 +242,4 @@
int next_point_index = 0;
copy_range_to_dst(first_range, next_point_index, dst_curves);
for (std::size_t i = 1; i < selected_ranges.size(); i++) {
Member

Use for (const int i : IndexRange(...)) {

Use `for (const int i : IndexRange(...)) { `
PRiera1 marked this conversation as resolved
@ -0,0 +274,4 @@
}();
const bke::CurvesGeometry &src_curves = *src_range.owning_curves;
const Array points_map = src_curves.point_to_curve_map();
Member

Always specify the type Array<int>. Same for all other places that just use Array.

Always specify the type `Array<int>`. Same for all other places that just use `Array`.
PRiera1 marked this conversation as resolved
@ -0,0 +364,4 @@
BLI_assert(opt_layer_index.has_value());
const int active_layer_index = *opt_layer_index;
IndexMaskMemory memory;
Member

Move this above the retrieve_selection_ranges call.

Move this above the `retrieve_selection_ranges` call.
PRiera1 marked this conversation as resolved
@ -0,0 +375,4 @@
auto [ranges_selected, selected_points_count] = retrieve_selection_ranges(
*object, retrieve_editable_drawings(*scene, grease_pencil), active_layer_index, memory);
if (ranges_selected.size() <= 1) {
// Nothing to join
Member
Comment style. See https://developer.blender.org/docs/handbook/guidelines/c_cpp/#comments
PRiera1 marked this conversation as resolved
@ -0,0 +380,4 @@
}
// Temporary geometry where to perform the logic
// Once the it gets stable, it is appended all at once to the destination curves
Member

Same as above.

Same as above.
PRiera1 marked this conversation as resolved
Pratik Borhade reviewed 2024-02-01 13:30:30 +01:00
Pratik Borhade left a comment
Member

Hi, did quick tests with your PR. Found some inconsistencies. Operator appears to work like mixture of "join stroke" and "merge points"
(current code is not splitting the new curve entirely. It also deletes points from selection)

Hi, did quick tests with your PR. Found some inconsistencies. Operator appears to work like mixture of "join stroke" and "merge points" (current code is not splitting the new curve entirely. It also deletes points from selection)
@ -0,0 +318,4 @@
}
}
void remove_selected_points_in_active_layer(Span<PointsRange> ranges_selected,
Member

Didn't quite understand the use of this
AFAICS legacy operators don't remove/dissolve points from existing selection

Didn't quite understand the use of this AFAICS legacy operators don't remove/dissolve points from existing selection
Author
First-time contributor

Hi, fair point.

The idea is to connect in a single stroke all the points selected. The selection may encompass many points from different layers and strokes. But the result will be just a single new stroke in the active layer's geometry, so basically a new element in the offsets array.

I first copy these points into the temporary curves, then delete them from the geometry, and add them back in the append function call. I did this in order to do not have overlapping points. So points are effectively moved from a stroke to another.

Maybe would make sense that this behavior could be configured from some modal checkbox, so the user could choose whether the existing selection should be copied or moved.

Hi, fair point. The idea is to connect in a single stroke all the points selected. The selection may encompass many points from different layers and strokes. But the result will be just a single new stroke in the active layer's geometry, so basically a new element in the offsets array. I first copy these points into the temporary curves, then delete them from the geometry, and add them back in the `append` function call. I did this in order to do not have overlapping points. So points are effectively moved from a stroke to another. Maybe would make sense that this behavior could be configured from some modal checkbox, so the user could choose whether the existing selection should be copied or moved.
Member

The idea is that we match the behavior of GPv2 if possible, which it seems to be in this case. I'd rather not make it more complicated by adding new options in the operator.

The idea is that we match the behavior of GPv2 if possible, which it seems to be in this case. I'd rather not make it more complicated by adding new options in the operator.
Pedro force-pushed gpv3_merge_points from 04e82350d2 to 46fea7a74c 2024-02-01 19:05:04 +01:00 Compare
Pedro added 1 commit 2024-02-01 19:12:17 +01:00
Author
First-time contributor

Hi, been checking the kd-tree approach and I'm not sure it is feasible, or that the performance benefits would be noticeable. Here's the issue:

The core of the algorithm (implemented between copy_point_attributes and compute_closest_range_to) is as follows:

working_range = selected_ranges.first();
for (i: IndexRange(1,  ranges.size() -1)) {
    closest_range_index = []() {
        for (n: IndexRange(i,  ranges.size() -1) {
            // compute the distance between working_range and ranges[n]
        } 
        // return the index k of the range with minimum distance
     }
    swap(ranges[i + 1], ranges[closest_range_index]);
    // Extend working_range with the data in ranges[i +1]
    // Operate on the geometry described by working_range
}

Basically what this is doing is to exclude the processed ranges to be queried again. This is important because once a range of points has been merged, it cannot be merged again. It has to be discarded from the subsequent lookups, otherwise there could be artifacts. Notice that working_rage is the progressive concatenation of the ranges in selected_ranges.

The kd-tree should make possible to remove the innermost loop. But in order to work properly the found nodes would need to be extracted from the tree, and I did not see in its API the possibility to do this. So in order to emulate this behavior, I would have to instantiate a new tree for each iteration.

In terms of complexity my algorithm does (n - 1) + (n - 2) + ... + 1 iterations. I don't think that's really bad, taking into account n is not the number of points selected, but the number of selected point ranges.

This comment is a long way to say that I don't see the way to fit the kd-tree in the current algorithm, and could use some hint if you decide that the tree has to be present in the implementation. Tagging @filedescriptor and @PratikPB2123

Hi, been checking the kd-tree approach and I'm not sure it is feasible, or that the performance benefits would be noticeable. Here's the issue: The core of the algorithm (implemented between `copy_point_attributes` and `compute_closest_range_to`) is as follows: ```Cpp working_range = selected_ranges.first(); for (i: IndexRange(1, ranges.size() -1)) { closest_range_index = []() { for (n: IndexRange(i, ranges.size() -1) { // compute the distance between working_range and ranges[n] } // return the index k of the range with minimum distance } swap(ranges[i + 1], ranges[closest_range_index]); // Extend working_range with the data in ranges[i +1] // Operate on the geometry described by working_range } ``` Basically what this is doing is to exclude the processed ranges to be queried again. This is important because once a range of points has been merged, it cannot be merged again. It has to be discarded from the subsequent lookups, otherwise there could be artifacts. Notice that `working_rage` is the progressive concatenation of the ranges in `selected_ranges`. The kd-tree should make possible to remove the innermost loop. But in order to work properly the found nodes would need to be extracted from the tree, and I did not see in its API the possibility to do this. So in order to emulate this behavior, I would have to instantiate a new tree for each iteration. In terms of complexity my algorithm does `(n - 1) + (n - 2) + ... + 1` iterations. I don't think that's really bad, taking into account `n` is not the number of points selected, but the number of selected point ranges. This comment is a long way to say that I don't see the way to fit the kd-tree in the current algorithm, and could use some hint if you decide that the tree has to be present in the implementation. Tagging @filedescriptor and @PratikPB2123
casey-bianco-davis requested changes 2024-02-01 23:15:35 +01:00
casey-bianco-davis left a comment
Member

Overall the code is looking good but currently the operator behaves differently on the active layer vs all other layers, I think it should behave the same on all layers have a option for removing the old selected points. I also think the Ctrl J key-bind should be added.

Overall the code is looking good but currently the operator behaves differently on the active layer vs all other layers, I think it should behave the same on all layers have a option for removing the old selected points. I also think the `Ctrl J` key-bind should be added.
@ -0,0 +291,4 @@
dst_curve_offsets,
IndexMask({first_selected_curve, 1}),
dst_curves.attributes_for_write());
dst_curves.cyclic_for_write()[0] = false;

dst_curves.cyclic_for_write().first() = false;

dst_curves.cyclic_for_write().first() = false;

Hi, been checking the kd-tree approach and I'm not sure it is feasible, or that the performance benefits would be noticeable

I think it is really easy to notice difference in results of N*N and N*log2(N)*2 on something like 1.000.000-points curves overlay of procedural scene generated by geometry nodes as grease pencil.
So, yeah, any trees much better then just loop(loop(loop(...))).

> Hi, been checking the kd-tree approach and I'm not sure it is feasible, or that the performance benefits would be noticeable I think it is really easy to notice difference in results of `N*N` and `N*log2(N)*2` on something like 1.000.000-points curves overlay of procedural scene generated by geometry nodes as grease pencil. So, yeah, any trees much better then just loop(loop(loop(...))).
Author
First-time contributor

Hello @mod_moder , regarding your comments:

  • Regarding the reverse_points, I'd suggest you to comment those lines and check the results. You'll see it is not at all that satisfactory. So please, first understand the code before saying things "are a completely wrong thing"
  • The tree. Ok, but how to introduce the tree into the algorithm? With the current API, I'd need to create a new tree for every iteration, and totally that would decrease performance. But you give no hint here, only criticize. And also, should community developers be concerned about advanced features like geometry nodes? I picked-up the task from the community tasks lists, which I suppose they should be easy entry points.

Anyway, my real point here is, I don't wake up in the morning to find out that the time and effort I've spent here just out of my goodwill is criticized unconstructively, without giving help on how to improve things. And certainly I won't spend my energy here. Also, I have no clue when this task would be completed, every guy casually passing by gives his opinion and I have no clear directions.

So, I'm done with this project, will go to spend my time somewhere else. My sincere gratitude however to @filedescriptor , @PratikPB2123 and the other developers who were welcoming and polite. Thank you!

Hello @mod_moder , regarding your comments: - Regarding the `reverse_points`, I'd suggest you to comment those lines and check the results. You'll see it is not at all that satisfactory. So please, first understand the code before saying things "are a completely wrong thing" - The tree. Ok, but how to introduce the tree into the algorithm? With the current API, I'd need to create a new tree for _every_ iteration, and totally that would decrease performance. But you give no hint here, only criticize. And also, should community developers be concerned about advanced features like geometry nodes? I picked-up the task from the community tasks lists, which I suppose they should be easy entry points. Anyway, my real point here is, I don't wake up in the morning to find out that the time and effort I've spent here just out of my goodwill is criticized unconstructively, without giving help on how to improve things. And certainly I won't spend my energy here. Also, I have no clue when this task would be completed, every guy casually passing by gives his opinion and I have no clear directions. So, I'm done with this project, will go to spend my time somewhere else. My sincere gratitude however to @filedescriptor , @PratikPB2123 and the other developers who were welcoming and polite. Thank you!
Author
First-time contributor

Closing

Closing
Pedro closed this pull request 2024-02-03 08:49:09 +01:00
Pedro deleted branch gpv3_merge_points 2024-02-03 08:49:25 +01:00
Member

Hi, I think PR was almost ready. Not sure why it got closed :/

Hi, I think PR was almost ready. Not sure why it got closed :/
Author
First-time contributor

My apologies. Some things are no justification for an excessive response. I'm sincerely sorry.

Regarding the task, I think you're right and this is almost ready. Besides some style issue here and there and the keyboard biding, AFAIK the main thing missing is to put a checkbox in a modal so the user can choose whether:

  • To copy the selection in the active layer in the new stroke (so the behavior will be the same in all layers)
  • To move the selection in the active layer to the new stroke. This is how it is working now, and from the user's PoV, I think this would make sense in order to re-arrange strokes.

Will be working on this during the next days, let me know if some other addition would be helpful.

My apologies. Some things are no justification for an excessive response. I'm sincerely sorry. Regarding the task, I think you're right and this is almost ready. Besides some style issue here and there and the keyboard biding, AFAIK the main thing missing is to put a checkbox in a modal so the user can choose whether: - To copy the selection in the active layer in the new stroke (so the behavior will be the same in all layers) - To move the selection in the active layer to the new stroke. This is how it is working now, and from the user's PoV, I think this would make sense in order to re-arrange strokes. Will be working on this during the next days, let me know if some other addition would be helpful.
Member

Branch is deleted so PR can not be reopened :/

Branch is deleted so PR can not be reopened :/
Member

The PR can be re-created again, that's not a problem.

The PR can be re-created again, that's not a problem.
Author
First-time contributor

Yep, I have still my local branch. Will push it again once I've finished the changes. I'm really sorry for the mess. Will keep you posted. Thanks for the attention and the understanding.

Yep, I have still my local branch. Will push it again once I've finished the changes. I'm really sorry for the mess. Will keep you posted. Thanks for the attention and the understanding.

It would be nice if we also changed the name of this PR to Join Operator as we discussed above to avoid confusion.

It would be nice if we also changed the name of this PR to Join Operator as we discussed above to avoid confusion.
Author
First-time contributor

Hi, it looks like I couldn't directly reopen this PR, so I created a new one: #117916

I think everything is pretty much addressed. Let's continue the conversation there for anything that might be missing

Hi, it looks like I couldn't directly reopen this PR, so I created a new one: https://projects.blender.org/blender/blender/pulls/117916 I think everything is pretty much addressed. Let's continue the conversation there for anything that might be missing

Pull request closed

Sign in to join this conversation.
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
8 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#116726
No description provided.