Curves: Transform Bezier handles #120222

Merged
Jacques Lucke merged 19 commits from laurynas/blender:transform-bezier-handles into main 2024-04-26 09:32:39 +02:00
Contributor

Allows user to transform Bezier handles.

Allows user to transform Bezier handles. <video src="/attachments/e96aa8b4-7f01-4d0f-b084-62479b079ecb" title="transform_demo.mov" controls></video>
Laurynas Duburas added 1 commit 2024-04-03 18:19:14 +02:00
Laurynas Duburas changed title from Curves: Transform Bezier handles to WIP: Curves: Transform Bezier handles 2024-04-03 18:19:21 +02:00
Iliya Katushenock added this to the Nodes & Physics project 2024-04-03 18:22:36 +02:00
Iliya Katushenock added the
Module
Modeling
label 2024-04-03 18:22:43 +02:00
Laurynas Duburas added 1 commit 2024-04-06 21:45:38 +02:00
Jacques Lucke requested review from Jacques Lucke 2024-04-09 13:09:17 +02:00
Jacques Lucke reviewed 2024-04-11 15:22:52 +02:00
@ -82,3 +133,2 @@
else {
selection_per_object[i] = ed::curves::retrieve_selected_points(curves, memory);
tc.data_len = selection_per_object[i].size();
tc.data_len = std::reduce(
Member

This call to std::reduce does not compile for me. It seems like the lambda has an invalid signature.

This call to `std::reduce` does not compile for me. It seems like the lambda has an invalid signature.
Author
Contributor

If to comment it, does another std::reduce in the same file compile?
Lambda's return value type is actually int64_t, it should match init value type, which is int.
Can you try casting return expression to int?
If it's not the reason, I'll rewrite it without reduce as it is not used in Blender with that function parameter and I don't know what else could cause this.

If to comment it, does another `std::reduce` in the same file compile? Lambda's return value type is actually `int64_t`, it should match init value type, which is `int`. Can you try casting return expression to `int`? If it's not the reason, I'll rewrite it without `reduce` as it is not used in Blender with that function parameter and I don't know what else could cause this.
Member

Both calls to reduce don't work for me. Was just testing this on compiler explorer: https://godbolt.org/z/bGcj89ejv
Note that it compiles with -stdlib=libc++ but not without.

Better just rewrite it.

Both calls to `reduce` don't work for me. Was just testing this on compiler explorer: https://godbolt.org/z/bGcj89ejv Note that it compiles with `-stdlib=libc++` but not without. Better just rewrite it.
JacquesLucke marked this conversation as resolved
Laurynas Duburas added 1 commit 2024-04-12 13:39:41 +02:00
Member

Do you consider this patch done or have you planned any further changes? If it's done from your side right now, please remove the "WIP" prefix.

Do you consider this patch done or have you planned any further changes? If it's done from your side right now, please remove the "WIP" prefix.
Laurynas Duburas added 1 commit 2024-04-12 13:43:37 +02:00
Author
Contributor

Not quite yet.
I've commented call curves.calculate_bezier_auto_handles();. With it Blender crashes in random places if to move Bezier handles. Sometimes in Vector destructor, sometimes on buffer swap. I am stuck on this.
Also had intentions to look more on two aligned handles constraint, but it requires more investigation on legacy curves code and I'm a bit busy these days.

If to take these two out from scope it is done.

Not quite yet. I've commented call `curves.calculate_bezier_auto_handles();`. With it Blender crashes in random places if to move Bezier handles. Sometimes in `Vector` destructor, sometimes on buffer swap. I am stuck on this. Also had intentions to look more on two aligned handles constraint, but it requires more investigation on legacy curves code and I'm a bit busy these days. If to take these two out from scope it is done.
Laurynas Duburas added 1 commit 2024-04-12 14:30:46 +02:00
Laurynas Duburas added 1 commit 2024-04-13 19:17:05 +02:00
a3d10274b1 Implicit sharing problem workaround
In the scope of `TransConvertType_Curve.create_trans_data` call method `handle_positions_left_for_write` is called and it's return value is left referenced by `TransInfo.data_container.data.loc`s. Latter after few frames in the scope of `TransConvertType_Curve.recalcData_curve` call method `handle_positions_left_for_write` gets called again, but layer's `sharing_info` has 2 strong users and layer gets copied and latter deleted (probably then second strong user also gets deleted) though layer's data is still referenced by `TransInfo`. To avoid this same reference to layer's data is shared between `TransConvertType_Curve.create_trans_data` and `TransConvertType_Curve.recalcData_curve` during all transformation process.
Author
Contributor

@JacquesLucke I've root of the problem mentioned above, maybe you could look it through. It can be observed in main branch also. In transform_convert_curves.cc replace curves.calculate_bezier_auto_handles() with curves.positions_for_write(). After few frames of moving selected vertex Blender should crash.

In the scope of TransConvertType_Curve.create_trans_data call method handle_positions_left_for_write is called and it's return value is left referenced by TransInfo.data_container.data.locs. Latter after few frames in the scope of TransConvertType_Curve.recalcData_curve call method handle_positions_left_for_write gets called again, but layer's sharing_info has 2 strong users and layer gets copied and latter deleted (probably when second strong user gets deleted) though layer's data is still referenced by TransInfo.
I don't fully understand all mechanisms behind, but came up with workaround a3d10274b1. Same reference to layer's data is shared between TransConvertType_Curve.create_trans_data and TransConvertType_Curve.recalcData_curve during all transformation process.

@JacquesLucke I've root of the problem mentioned above, maybe you could look it through. It can be observed in `main` branch also. In `transform_convert_curves.cc` replace `curves.calculate_bezier_auto_handles()` with `curves.positions_for_write()`. After few frames of moving selected vertex Blender should crash. In the scope of `TransConvertType_Curve.create_trans_data` call method `handle_positions_left_for_write` is called and it's return value is left referenced by `TransInfo.data_container.data.loc`s. Latter after few frames in the scope of `TransConvertType_Curve.recalcData_curve` call method `handle_positions_left_for_write` gets called again, but layer's `sharing_info` has 2 strong users and layer gets copied and latter deleted (probably when second strong user gets deleted) though layer's data is still referenced by `TransInfo`. I don't fully understand all mechanisms behind, but came up with workaround a3d10274b1. Same reference to layer's data is shared between `TransConvertType_Curve.create_trans_data` and `TransConvertType_Curve.recalcData_curve` during all transformation process.
Member

There is indeed a more fundamental problem in the transform code currently... I created an initial patch to address it: #120631.

There is indeed a more fundamental problem in the transform code currently... I created an initial patch to address it: #120631.
Laurynas Duburas added 1 commit 2024-04-22 22:40:50 +02:00
Laurynas Duburas added 1 commit 2024-04-22 23:18:59 +02:00
Laurynas Duburas added 2 commits 2024-04-23 09:22:54 +02:00
Author
Contributor

I decided to leave Aligned and some other handle type dynamics for another patch.

I decided to leave `Aligned` and some other handle type dynamics for another patch.
Laurynas Duburas changed title from WIP: Curves: Transform Bezier handles to Curves: Transform Bezier handles 2024-04-23 09:39:36 +02:00
Laurynas Duburas requested review from Hans Goudey 2024-04-23 09:39:51 +02:00
Jacques Lucke requested changes 2024-04-23 11:58:16 +02:00
Dismissed
Jacques Lucke left a comment
Member

My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler.

My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler.
@ -67,0 +72,4 @@
int offset = 0;
for (const int i : src_offsets.index_range()) {
dst_offsets[i] = offset;
offset += src_offsets[i].size() * (selection.contains(i) ? multiplier : 1);
Member

Using IndexMask.contains should generally be avoided if possible. I wonder if it would be useful to have another iterator like selection.foreach_in_range(IndexRange(...), [&](int i, bool contained) { ... }). That could be implemented much more efficiently than calling selection.contains for each index separately.

Using `IndexMask.contains` should generally be avoided if possible. I wonder if it would be useful to have another iterator like `selection.foreach_in_range(IndexRange(...), [&](int i, bool contained) { ... })`. That could be implemented much more efficiently than calling `selection.contains` for each index separately.
Author
Contributor

I've refactored expand_selected_offsets. It would be much simpler with proposed iterator, but for now it will do.

I've refactored `expand_selected_offsets`. It would be much simpler with proposed iterator, but for now it will do.
laurynas marked this conversation as resolved
@ -96,0 +116,4 @@
curves.curves_range(),
curves_transform_data->memory);
/* Alter selection as in legacy curves bezt_select_to_transform_triple_flag(). */
if (bezier_curves[i].size()) {
Member

Use !.is_empty()

Use `!.is_empty()`
laurynas marked this conversation as resolved
@ -96,0 +138,4 @@
IndexMask must_be_selected_mask = IndexMask::from_indices(must_be_selected.as_span(),
curves_transform_data->memory);
if (must_be_selected.size()) {
selection_per_attribute[i][1] = IndexMask::from_union(
Member

/* Select bezier handles that must be transformed if the main control point is selected. */

`/* Select bezier handles that must be transformed if the main control point is selected. */`
laurynas marked this conversation as resolved
@ -96,0 +145,4 @@
}
}
int positions_in_custom_data = 0;
Member

Is this really a position or really just points_to_transform_num?

Is this really a position or really just `points_to_transform_num`?
laurynas marked this conversation as resolved
@ -169,0 +233,4 @@
curves.positions_for_write(),
curves.handle_positions_left_for_write(),
curves.handle_positions_right_for_write()};
for (const int layer :
Member

Why do we suddenly have multiple layers? Seems like this is mixing layers and selection attribute names.

Why do we suddenly have multiple layers? Seems like this is mixing layers and selection attribute names.
@ -215,2 +288,4 @@
const blender::IndexMask &bezier_curves)
{
using namespace blender;
/* Term layer in function is used to refer Bezier curve points, left and right handles as layers
Member

"Layer" really should not be used in this context.

"Layer" really should not be used in this context.
Author
Contributor

I doubted about this terminology myself, but it seemed nice in a context where positions from different domains are joined into one flat array. I'll think how to remove it.

I doubted about this terminology myself, but it seemed nice in a context where positions from different domains are joined into one flat array. I'll think how to remove it.
Member

I wonder if we should just go for a much simple solution first where we just make a full copy of the position arrays. That way we can skip a lot of this complex index handling for now. That can be added separately when we see that it actually is a bottleneck.

I wonder if we should just go for a much simple solution first where we just make a full copy of the position arrays. That way we can skip a lot of this complex index handling for now. That can be added separately when we see that it actually is a bottleneck.
Author
Contributor

My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler.

My first impression that you are right here. I just saw that data structure from #120824 can be reused with little changes.

I wonder if we should just go for a much simple solution first where we just make a full copy of the position arrays. That way we can skip a lot of this complex index handling for now. That can be added separately when we see that it actually is a bottleneck.

All indexing complexity is in use_proportional_edit and it actually comes from use_connected_only case. For this one to work points has to be arranged in particular order. I don't know maybe that problem could be push down to calculate_curve_point_distances_for_proportional_editing, but for me it was easier solution.
And regarding full copy that is what I'm doing. It it possible to copy only handle_positions_left/right for Bezier curves, but I quit that idea :)

> My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler. My first impression that you are right here. I just saw that data structure from #120824 can be reused with little changes. >I wonder if we should just go for a much simple solution first where we just make a full copy of the position arrays. That way we can skip a lot of this complex index handling for now. That can be added separately when we see that it actually is a bottleneck. All indexing complexity is in `use_proportional_edit` and it actually comes from `use_connected_only` case. For this one to work points has to be arranged in particular order. I don't know maybe that problem could be push down to `calculate_curve_point_distances_for_proportional_editing`, but for me it was easier solution. And regarding full copy that is what I'm doing. It it possible to copy only handle_positions_left/right for Bezier curves, but I quit that idea :)
Laurynas Duburas added 1 commit 2024-04-23 12:38:59 +02:00
Iliya Katushenock reviewed 2024-04-23 12:41:16 +02:00
@ -149,6 +149,11 @@ void gather_group_sizes(OffsetIndices<int> offsets, const IndexMask &mask, Mutab
void gather_group_sizes(OffsetIndices<int> offsets, Span<int> indices, MutableSpan<int> sizes);
OffsetIndices<int> expand_selected_offsets(const OffsetIndices<int> src_offsets,

Not sure this function is so general to be in this file\

Not sure this function is so general to be in this file\
Author
Contributor

Agree, it is not.

Agree, it is not.
laurynas marked this conversation as resolved
Laurynas Duburas added 1 commit 2024-04-23 13:43:17 +02:00
Iliya Katushenock reviewed 2024-04-23 14:00:41 +02:00
@ -173,6 +248,33 @@ static void recalcData_curves(TransInfo *t)
}
}
static OffsetIndices<int> expand_selected_offsets(const OffsetIndices<int> src_offsets,
offset_indices::copy_group_sizes(src_offsets, src_offsets.index_range(), dst_offsets);
selection.foreah_index_optimized<int>(GrainSize(4096),
                                      [factor](const int i) { dst_offsets[i] *= factor; });
offset_indices::accumulate_counts_to_offsets(dst_offsets);
```Cpp offset_indices::copy_group_sizes(src_offsets, src_offsets.index_range(), dst_offsets); selection.foreah_index_optimized<int>(GrainSize(4096), [factor](const int i) { dst_offsets[i] *= factor; }); offset_indices::accumulate_counts_to_offsets(dst_offsets); ```
laurynas marked this conversation as resolved
Laurynas Duburas added 1 commit 2024-04-23 14:14:14 +02:00
Laurynas Duburas added 1 commit 2024-04-23 15:26:36 +02:00
Laurynas Duburas added 1 commit 2024-04-24 10:44:44 +02:00
Laurynas Duburas added 1 commit 2024-04-24 12:33:55 +02:00
buildbot/vexp-code-patch-lint Build done. Details
buildbot/vexp-code-patch-linux-x86_64 Build done. Details
buildbot/vexp-code-patch-darwin-x86_64 Build done. Details
buildbot/vexp-code-patch-windows-amd64 Build done. Details
buildbot/vexp-code-patch-darwin-arm64 Build done. Details
buildbot/vexp-code-patch-coordinator Build done. Details
c1441e3c6e
return value to append_positions_to_custom_data
Author
Contributor

My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler.

Separate arrays wouldn't simplify things. I've added return value to append_positions_to_custom_data, now all separation is done inside of function and it is one line anyway.

There is tricky part that GP layers and curves Bezier handles are treaded equally by CurvesTransformData. Code as is will require extra parameter (or layer shift in calling code) for copy_positions_from_curves_transform_custom_data to support GP with Bezier handles transformation. Still any ideas I came with on restructuring data would not simplify anything.

Here is positions layout scheme . All complexity comes from proportional connected case. It requires points to be grouped by curves and arranged that handles are next to their point. I tried to separate this case, but only managed to complicate things more.

point mapping.jpg


Finally in both proportional and normal cases 'Normal transform case' layout is used.

> My main high level comment is that it feels like it may be easier not to put everything into a single position array. It's ok, but I wonder if it could become simpler. Separate arrays wouldn't simplify things. I've added return value to `append_positions_to_custom_data`, now all separation is done inside of function and it is one line anyway. There is tricky part that GP layers and curves Bezier handles are treaded equally by `CurvesTransformData`. Code as is will require extra parameter (or `layer` shift in calling code) for `copy_positions_from_curves_transform_custom_data` to support GP with Bezier handles transformation. Still any ideas I came with on restructuring data would not simplify anything. Here is positions layout scheme . All complexity comes from proportional connected case. It requires points to be grouped by curves and arranged that handles are next to their point. I tried to separate this case, but only managed to complicate things more. ![point mapping.jpg](/attachments/a3f0f16b-33ef-4d41-b06a-2b78648d848e) ______________________ Finally in both proportional and normal cases 'Normal transform case' layout is used.
Member

All complexity comes from proportional connected case.
Is that generally true, or only because of the way calculate_curve_point_distances_for_proportional_editing is implemented currently?

Overall, it feels like one needs to take a step back and maybe just rewrite the whole transform code from scratch instead of adding more complexity. Even more complexity will be necessary to handle the tilt and radius modes too.

I'll give this another test run now, and if I don't find functional issues, I'll accept this for now. I might rewrite this thing from scratch at some point (unless you still want to work on it).

Seems like a more productive way forward to get this basic functionality working on a user-level now, so we can make progress on the project overall. The transform code is well enough isolated that it seems reasonable to refactor it later on without affecting other areas.

> All complexity comes from proportional connected case. Is that generally true, or only because of the way `calculate_curve_point_distances_for_proportional_editing` is implemented currently? Overall, it feels like one needs to take a step back and maybe just rewrite the whole transform code from scratch instead of adding more complexity. Even more complexity will be necessary to handle the tilt and radius modes too. I'll give this another test run now, and if I don't find functional issues, I'll accept this for now. I might rewrite this thing from scratch at some point (unless you still want to work on it). Seems like a more productive way forward to get this basic functionality working on a user-level now, so we can make progress on the project overall. The transform code is well enough isolated that it seems reasonable to refactor it later on without affecting other areas.
Jacques Lucke approved these changes 2024-04-24 15:36:12 +02:00
Member

@blender-bot build

@blender-bot build
Author
Contributor

I have a refactoring idea. It is not total rewrite, but will look better.

I have a refactoring idea. It is not total rewrite, but will look better.
Laurynas Duburas changed title from Curves: Transform Bezier handles to WIP: Curves: Transform Bezier handles 2024-04-25 01:08:54 +02:00
Laurynas Duburas added 1 commit 2024-04-25 13:16:40 +02:00
Member

Please remove WIP from the name again once you're ready.

Please remove WIP from the name again once you're ready.
Laurynas Duburas added 1 commit 2024-04-25 14:40:06 +02:00
Laurynas Duburas changed title from WIP: Curves: Transform Bezier handles to Curves: Transform Bezier handles 2024-04-25 14:40:36 +02:00
Hans Goudey requested changes 2024-04-25 16:58:23 +02:00
Dismissed
Hans Goudey left a comment
Member

At a high level, I think the only thing I really care about is that there's no performance cost when there are no Bezier curves. I didn't thoroughly check whether that was the case, it's a bit hard to tell.

At a high level, I think the only thing I really care about is that there's no performance cost when there are no Bezier curves. I didn't thoroughly check whether that was the case, it's a bit hard to tell.
@ -95,0 +127,4 @@
if (ELEM(type_left, BEZIER_HANDLE_AUTO, BEZIER_HANDLE_ALIGN) &&
ELEM(type_right, BEZIER_HANDLE_AUTO, BEZIER_HANDLE_ALIGN))
{
must_be_selected.append(point_i);
Member

Appending from a threaded loop isn't threadsafe

Appending from a threaded loop isn't threadsafe
Author
Contributor

Moved to sequential foreach_index.

Moved to sequential `foreach_index`.
JacquesLucke marked this conversation as resolved
@ -176,0 +259,4 @@
}
/**
* Creates map of indexes to `tc.data` representing curve in layout
Member

indexes -> indices

`indexes` -> `indices`
laurynas marked this conversation as resolved
@ -176,0 +265,4 @@
*/
static void fill_map(const CurveType curve_type,
MutableSpan<int> map,
IndexRange curve_points,
Member

More the map argument last, use const for the others

More the `map` argument last, use const for the others
laurynas marked this conversation as resolved
@ -240,0 +410,4 @@
const int selection_attrs_num = curve_types[curve_i] == CURVE_TYPE_BEZIER ? 3 : 1;
const IndexRange curve_points = points_by_curve[curve_i];
const int total_curve_points = selection_attrs_num * curve_points.size();
map.reinitialize(total_curve_points);
Member

I don't think this is threadsafe (adjusting the vectors from inside this threaded loop)

I don't think this is threadsafe (adjusting the vectors from inside this threaded loop)
laurynas marked this conversation as resolved
@ -117,3 +116,2 @@
value_attribute,
points,
use_proportional_edit,
Span(&points, 1),
Member

I think {points} will work?

I think {points} will work?
laurynas marked this conversation as resolved
Laurynas Duburas added 1 commit 2024-04-25 19:31:47 +02:00
Hans Goudey approved these changes 2024-04-25 19:46:21 +02:00
Author
Contributor

At a high level, I think the only thing I really care about is that there's no performance cost when there are no Bezier curves. I didn't thoroughly check whether that was the case, it's a bit hard to tell.

If to ignore extra assignments in array initialization areas, I see two moments in use_connected_only case.
Initialization of bezier_offsets_in_td.

    Array<int> bezier_offsets_in_td(curves.curves_num() + 1, 0);
    offset_indices::copy_group_sizes(points_by_curve, bezier_curves, bezier_offsets_in_td);
    offset_indices::accumulate_counts_to_offsets(bezier_offsets_in_td);

Second without Bezier mapwouldn't be needed. It adds indirection to referencing.
Now I kind of see that mapcould be replaced with extra forloop's as it consist of three starting points and walks with respective steps.
I don't know if it is worth adding this noise to code.

> At a high level, I think the only thing I really care about is that there's no performance cost when there are no Bezier curves. I didn't thoroughly check whether that was the case, it's a bit hard to tell. If to ignore extra assignments in array initialization areas, I see two moments in `use_connected_only` case. Initialization of `bezier_offsets_in_td`. ```c Array<int> bezier_offsets_in_td(curves.curves_num() + 1, 0); offset_indices::copy_group_sizes(points_by_curve, bezier_curves, bezier_offsets_in_td); offset_indices::accumulate_counts_to_offsets(bezier_offsets_in_td); ``` Second without Bezier `map`wouldn't be needed. It adds indirection to referencing. Now I kind of see that `map`could be replaced with extra `for`loop's as it consist of three starting points and walks with respective steps. I don't know if it is worth adding this noise to code.
Jacques Lucke merged commit 12df5a68ba into main 2024-04-26 09:32:39 +02:00
Laurynas Duburas deleted branch transform-bezier-handles 2024-04-26 10:11:41 +02:00
Sign in to join this conversation.
No reviewers
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 Assignees
4 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#120222
No description provided.