GPv3: Merge Points #116726
No reviewers
Labels
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
No due date set.
Dependencies
No dependencies set.
Reference: blender/blender#116726
Loading…
Reference in New Issue
No description provided.
Delete Branch "(deleted):gpv3_merge_points"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
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.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>
@ -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.
@ -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 useIndexRange
instead ofauto
@ -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
withinfo.drawing.strokes_for_write()
and all of the
continue
s would need to be replaced withreturn
s.#113582 is indeed outdated 😅
@ -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.@ -1641,0 +1750,4 @@
}
IndexMaskMemory memory;
const bke::CurvesGeometry &geometry = drawing->strokes();
The name
curves
has been used for theCurvesGeometry
i.e.
bke::CurvesGeometry &curves = info.drawing.strokes_for_write();
@ -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 ofretrieve_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);
@ -1641,0 +1757,4 @@
continue;
}
const Vector<IndexRange> src_ranges = compute_src_ranges(points_selection, geometry);
I think this
Vector
can be replaced when aSpan
orArray
@ -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:
after the attributes are copy.
And make
geometry
notconst
@ -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 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 theupdate...
function. At the moment I was unaware of that.Crash likely to happen when curve attributes are not updated.
@ -1641,0 +1771,4 @@
act_drawing->tag_topology_changed();
act_geometry.update_curve_types();
return OPERATOR_FINISHED;
It's important to tag for updates.
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.
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> {
i think source range should be "selection" range, you can use
retrieve_editable_and_selected_points
instead@ -1641,0 +1675,4 @@
return final_ranges;
};
auto resize_and_compute_new_offsets =
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)
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.
You can refer legacy GP operator for the final result. Offset length will be 2 here as said earlier :)
If you make offset 2 and update curve-domain attributes properly, all selected points of new stroke will be connected.
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
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@ -1641,0 +1773,4 @@
bool changed = false;
threading::parallel_for_each(drawings, [&](MutableDrawingInfo &info) {
IndexMaskMemory memory;
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.WIP: #113582 gpv3_merge_pointsto WIP: gpv3_merge_pointsJust 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.
WIP: gpv3_merge_pointsto WIP: GPv3: Merge Points7aa1195b9e
to3f7b34b400
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.
Hope the implementation looks good now. I have some questions/comments anyway and I hope they'll help to improve the solution:
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?GreasePencil
struct:std::optional<int> get_layer_index(const *Layer) const
This way the code computing theactive_layer_index
wouldn't be necessaryDid 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;
@HooglyBoogly Do you agree with this change?
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 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@ -1668,1 +1670,4 @@
namespace stroke_merge_utils {
struct CurveRange {
IndexMask range_mask;
Why is this not an
IndexRange
?my bad. Fixed
@ -1669,0 +1681,4 @@
bool is_active_layer;
};
static Array<CurveRange> compute_stroke_ranges(IndexMask selection,
Use
const IndexMask &selection
@ -1669,0 +1725,4 @@
return final_ranges.as_span();
}
static Array<LayerSelectionInfo> obtain_selections_by_layer(
->
retrieve_selections_by_layer
. "retrieve" is what's used elsewhere, so better to make this consistent.@ -1669,0 +1732,4 @@
IndexMaskMemory &memory)
{
Vector<LayerSelectionInfo> selections_by_layer;
for (MutableDrawingInfo info : frame_drawings) {
Use
MutableDrawingInfo &
to avoid a copy@ -1669,0 +1777,4 @@
IndexMaskMemory memory;
Array<int> mask_values(info.selected_ranges.size());
for (const auto index : mask_values.index_range()) {
use
const int index
@ -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;
This line is a bit confusing. Please add a comment that explains why the
index
is doubled.@ -1669,0 +1792,4 @@
dst_curves.attributes_for_write());
}
static void remove_selection(bke::CurvesGeometry &curves, IndexMask points_selection)
Use
const IndexMask &
@ -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);
Use
Vector<MutableDrawingInfo> &
@ -1669,0 +1861,4 @@
}
Span<const Layer *> layers = grease_pencil.layers();
const auto it = std::find(layers.begin(), layers.end(), active_layer);
Use
layers.find(active_layer)
and assert it's not-1
. Also remove the#include <algorithm>
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;
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.
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
@ -1669,0 +1684,4 @@
static Array<CurveRange> compute_stroke_ranges(IndexMask selection,
const bke::CurvesGeometry &curves)
{
const Span<IndexRange> initial_ranges = selection.to_ranges();
Span
->Vector
True, and I'm pretty astonished this ever worked. This is clearly UB
@ -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{};
final_ranges{};
->final_ranges;
@ -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();
points_map
->point_to_curve_map
@ -1669,0 +1722,4 @@
final_ranges.append({range, static_cast<CurveType>(curve_types[previous_curve])});
}
return final_ranges.as_span();
Here you're invoking the
Span
constructor ofArray
. Better to just return the vector to avoid a copy@ -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{};
Since you know the number of elements up front, use
Array
instead ofVector
and fill in the elements by index@ -1669,0 +1803,4 @@
static void merge_curve_attributes(const LayerSelectionInfo &info, bke::CurvesGeometry &dst_curves)
{
/**
Comments inside of functions don't get the doxygen style, they would look more like this:
@ -1669,0 +1844,4 @@
bool changed = false;
Map<int, Vector<MutableDrawingInfo>> drawings_per_frame{};
Remove
{}
here,Map
has a default constructor@ -1669,0 +1866,4 @@
const int active_layer_index = it - layers.begin();
IndexMaskMemory memory;
Array<LayerSelectionInfo> selections_by_layer = obtain_selections_by_layer(
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
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.
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.
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
I think the general idea for this transition is "when in doubt, keep the existing behavior". So sounds good
Generally, we only test lower level functions that are used in the operators. And operators should not have to implement lower-level functions themselves.
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?
No, a class is not needed.
Yes, this is needed in other places as well. I have commit ready for it.
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!
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.
I pushed
35e8959d77
:)3bed4bea05
tofb928d4d08
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.
Sounds good.
I think you can put the entire definition of the operator in this new file, and call it something like
grease_pencil_merge.cc
.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
605c56bd55
toe90eaa910e
WIP: GPv3: Merge Pointsto GPv3: Merge PointsHi, 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.
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.
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.
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
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";
Not sure what the convention is but AFAIK
idname
should match with function name?@ -0,0 +232,4 @@
bool changed = false;
Map<int, Vector<MutableDrawingInfo>> drawings_per_frame;
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
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
Yeah, the map is not needed. I'll fix it
I pushed the changes yesterday and there's no map anymore. I suppose this resolves this point and the next
Thanks. I'll check shortly 🙂
@ -0,0 +241,4 @@
const Layer *active_layer = grease_pencil.get_active_layer();
if (active_layer == nullptr) {
return OPERATOR_FINISHED;
OPEATOR_CANCELLED
@ -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);
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.
c57cf3b209
to9a8254fd5f
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)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)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);
Generally, the API for
CurvesGeometry
should be kept small. I think this function is not needed here and can just be a static function ingrease_pencil_join_selection
.I would also add that
append
duplicates some behavior ofgeometry::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);
Same as above, this can be a static function instead.
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@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.reverse_curves
which one is just change attribute of curve direction in convition way).@ -1422,0 +1423,4 @@
/** \name Stroke From Points Operator
* \{ */
extern int(grease_pencil_join_selection_exec)(bContext *C, wmOperator *op);
Instead, define
GREASE_PENCIL_OT_join_selection
ingrease_pencil_join_selection.cc
and declare it inED_grease_pencil.hh
. Then you can still pass it toWM_operatortype_append
in this file.Hopefully now is better
@ -0,0 +23,4 @@
namespace blender::ed::greasepencil {
namespace {
Can be removed.
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 usingstatic
)That being said, I'll remove the namespace and mark the internal functions static, is just that I wanted to justify this point
I understand. I haven't seen this much elsewhere so I thought I'd note it. You can keep it if you want.
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.
From the style guide itself about this point:
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 };
The name for the
enum class
should be camel case:ActionOnNextRange
@ -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(
I'd prefer if this returned just the
Vector
and wrote to an argumentint &r_total_points_selected
.@ -0,0 +46,4 @@
*/
std::pair<Vector<PointsRange>, int64_t> retrieve_selection_ranges(
Object &object,
Span<MutableDrawingInfo> drawings,
Can be
const
@ -0,0 +104,4 @@
}
void apply_action(ACTION_ON_NEXT_RANGE action,
IndexRange current_range,
Use
const
@ -0,0 +105,4 @@
void apply_action(ACTION_ON_NEXT_RANGE action,
IndexRange current_range,
IndexRange adding_range,
Use
const
@ -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,
Same as further above: I think this should return an int and write to an argument
ActionOnNextRange &r_action
@ -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++) {
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 ofIndexRange
is the size not the stop index. The correct way would befor (const int64_t i : ranges.range().drop_from(starting_from)) {
@ -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);
Use
math::distance_squared
@ -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++) {
Use
for (const int i : IndexRange(...)) {
@ -0,0 +274,4 @@
}();
const bke::CurvesGeometry &src_curves = *src_range.owning_curves;
const Array points_map = src_curves.point_to_curve_map();
Always specify the type
Array<int>
. Same for all other places that just useArray
.@ -0,0 +364,4 @@
BLI_assert(opt_layer_index.has_value());
const int active_layer_index = *opt_layer_index;
IndexMaskMemory memory;
Move this above the
retrieve_selection_ranges
call.@ -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
Comment style. See https://developer.blender.org/docs/handbook/guidelines/c_cpp/#comments
@ -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
Same as above.
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,
Didn't quite understand the use of this
AFAICS legacy operators don't remove/dissolve points from existing selection
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.
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.
04e82350d2
to46fea7a74c
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
andcompute_closest_range_to
) is as follows: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 inselected_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 accountn
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
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;
I think it is really easy to notice difference in results of
N*N
andN*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(...))).
Hello @mod_moder , regarding your comments:
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"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!
Closing
Hi, I think PR was almost ready. Not sure why it got closed :/
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:
Will be working on this during the next days, let me know if some other addition would be helpful.
Branch is deleted so PR can not be reopened :/
The PR can be re-created again, that's not a problem.
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.
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
Pull request closed