GPv3: Soft mode for the Eraser tool #110310
@ -397,7 +397,7 @@ struct EraseOperationExecutor {
|
||||
bool is_cut;
|
||||
|
||||
/* Additional attributes changes that can be stored to be used after a call to
|
||||
* recompute_topology.
|
||||
* compute_topology_change.
|
||||
* Note that they won't be automatically updated in the destination's attributes.
|
||||
*/
|
||||
float opacity;
|
||||
@ -838,217 +838,6 @@ struct EraseOperationExecutor {
|
||||
return total_intersections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a \a dst curves geometry by applying a change of topology from a \a src curves
|
||||
* geometry.
|
||||
* The change of topology is described by \a src_to_dst_points, which size should be
|
||||
* equal to the number of points in the source.
|
||||
* For each point in the source, the corresponding vector in \a src_to_dst_points contains a set
|
||||
* of destination points (PointTransferData), which can correspond to points of the source, or
|
||||
* linear combination of them. Note that this vector can be empty if we want to remove points for
|
||||
* example. Curves can also be splitted if a destination point is marked as a cut.
|
||||
*
|
||||
* \returns an array containing the same elements as \a src_to_dst_points, but in the destination
|
||||
* points domain.
|
||||
*/
|
||||
static Array<PointTransferData> recompute_topology(
|
||||
const bke::CurvesGeometry &src,
|
||||
bke::CurvesGeometry &dst,
|
||||
const Span<Vector<PointTransferData>> src_to_dst_points,
|
||||
const bool keep_caps)
|
||||
{
|
||||
const int src_curves_num = src.curves_num();
|
||||
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
|
||||
const VArray<bool> src_cyclic = src.cyclic();
|
||||
|
||||
int dst_points_num = 0;
|
||||
for (const Vector<PointTransferData> &src_transfer_data : src_to_dst_points) {
|
||||
dst_points_num += src_transfer_data.size();
|
||||
}
|
||||
if (dst_points_num == 0) {
|
||||
dst.resize(0, 0);
|
||||
return Array<PointTransferData>(0);
|
||||
}
|
||||
|
||||
/* Set the intersection parameters in the destination domain : a pair of int and float
|
||||
* numbers for which the integer is the index of the corresponding segment in the
|
||||
* source curves, and the float part is the (0,1) factor representing its position in
|
||||
* the segment.
|
||||
*/
|
||||
Array<PointTransferData> dst_transfer_data(dst_points_num);
|
||||
|
||||
Array<int> src_pivot_point(src_curves_num, -1);
|
||||
Array<int> dst_interm_curves_offsets(src_curves_num + 1, 0);
|
||||
int dst_point = -1;
|
||||
for (const int src_curve : src.curves_range()) {
|
||||
const IndexRange src_points = src_points_by_curve[src_curve];
|
||||
|
||||
for (const int src_point : src_points) {
|
||||
for (const PointTransferData &dst_point_transfer : src_to_dst_points[src_point]) {
|
||||
if (dst_point_transfer.is_src_point) {
|
||||
dst_transfer_data[++dst_point] = dst_point_transfer;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Add an intersection with the eraser and mark it as a cut. */
|
||||
dst_transfer_data[++dst_point] = dst_point_transfer;
|
||||
|
||||
/* For cyclic curves, mark the pivot point as the last intersection with the eraser
|
||||
* that starts a new segment in the destination.
|
||||
*/
|
||||
if (src_cyclic[src_curve] && dst_point_transfer.is_cut) {
|
||||
src_pivot_point[src_curve] = dst_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* We store intermediate curve offsets represent an intermediate state of the
|
||||
* destination curves before cutting the curves at eraser's intersection. Thus, it
|
||||
* contains the same number of curves than in the source, but the offsets are
|
||||
* different, because points may have been added or removed. */
|
||||
dst_interm_curves_offsets[src_curve + 1] = dst_point + 1;
|
||||
}
|
||||
|
||||
/* Cyclic curves. */
|
||||
Array<bool> src_now_cyclic(src_curves_num);
|
||||
threading::parallel_for(src.curves_range(), 4096, [&](const IndexRange src_curves) {
|
||||
for (const int src_curve : src_curves) {
|
||||
const int pivot_point = src_pivot_point[src_curve];
|
||||
|
||||
if (pivot_point == -1) {
|
||||
/* Either the curve was not cyclic or it wasn't cut : no need to change it. */
|
||||
src_now_cyclic[src_curve] = src_cyclic[src_curve];
|
||||
continue;
|
||||
}
|
||||
|
||||
/* A cyclic curve was cut :
|
||||
* - this curve is not cyclic anymore,
|
||||
* - and we have to shift points to keep the closing segment.
|
||||
*/
|
||||
src_now_cyclic[src_curve] = false;
|
||||
|
||||
const int dst_interm_first = dst_interm_curves_offsets[src_curve];
|
||||
const int dst_interm_last = dst_interm_curves_offsets[src_curve + 1];
|
||||
std::rotate(dst_transfer_data.begin() + dst_interm_first,
|
||||
dst_transfer_data.begin() + pivot_point,
|
||||
dst_transfer_data.begin() + dst_interm_last);
|
||||
}
|
||||
});
|
||||
|
||||
/* Compute the destination curve offsets. */
|
||||
Vector<int> dst_curves_offset;
|
||||
Vector<int> dst_to_src_curve;
|
||||
dst_curves_offset.append(0);
|
||||
for (int src_curve : src.curves_range()) {
|
||||
const IndexRange dst_points_range(dst_interm_curves_offsets[src_curve],
|
||||
dst_interm_curves_offsets[src_curve + 1] -
|
||||
dst_interm_curves_offsets[src_curve]);
|
||||
int length_of_current = 0;
|
||||
|
||||
for (int dst_point : dst_points_range) {
|
||||
|
||||
if ((length_of_current > 0) && dst_transfer_data[dst_point].is_cut) {
|
||||
/* This is the new first point of a curve. */
|
||||
dst_curves_offset.append(dst_point);
|
||||
dst_to_src_curve.append(src_curve);
|
||||
length_of_current = 0;
|
||||
}
|
||||
++length_of_current;
|
||||
}
|
||||
|
||||
if (length_of_current != 0) {
|
||||
/* End of a source curve. */
|
||||
dst_curves_offset.append(dst_points_range.one_after_last());
|
||||
dst_to_src_curve.append(src_curve);
|
||||
}
|
||||
}
|
||||
const int dst_curves_num = dst_curves_offset.size() - 1;
|
||||
if (dst_curves_num == 0) {
|
||||
dst.resize(0, 0);
|
||||
return dst_transfer_data;
|
||||
}
|
||||
|
||||
/* Build destination curves geometry. */
|
||||
dst.resize(dst_points_num, dst_curves_num);
|
||||
array_utils::copy(dst_curves_offset.as_span(), dst.offsets_for_write());
|
||||
const OffsetIndices<int> dst_points_by_curve = dst.points_by_curve();
|
||||
|
||||
/* Attributes. */
|
||||
const bke::AttributeAccessor src_attributes = src.attributes();
|
||||
bke::MutableAttributeAccessor dst_attributes = dst.attributes_for_write();
|
||||
const bke::AnonymousAttributePropagationInfo propagation_info{};
|
||||
|
||||
/* Copy curves attributes. */
|
||||
for (bke::AttributeTransferData &attribute : bke::retrieve_attributes_for_transfer(
|
||||
src_attributes, dst_attributes, ATTR_DOMAIN_MASK_CURVE, propagation_info, {"cyclic"}))
|
||||
{
|
||||
bke::attribute_math::gather(attribute.src, dst_to_src_curve.as_span(), attribute.dst.span);
|
||||
attribute.dst.finish();
|
||||
}
|
||||
array_utils::gather(
|
||||
src_now_cyclic.as_span(), dst_to_src_curve.as_span(), dst.cyclic_for_write());
|
||||
|
||||
/* Display intersections with flat caps. */
|
||||
if (!keep_caps) {
|
||||
bke::SpanAttributeWriter<int8_t> dst_start_caps =
|
||||
dst_attributes.lookup_or_add_for_write_span<int8_t>("start_cap", ATTR_DOMAIN_CURVE);
|
||||
bke::SpanAttributeWriter<int8_t> dst_end_caps =
|
||||
dst_attributes.lookup_or_add_for_write_span<int8_t>("end_cap", ATTR_DOMAIN_CURVE);
|
||||
|
||||
threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves) {
|
||||
for (const int dst_curve : dst_curves) {
|
||||
const IndexRange dst_curve_points = dst_points_by_curve[dst_curve];
|
||||
if (dst_transfer_data[dst_curve_points.first()].is_cut) {
|
||||
dst_start_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT;
|
||||
}
|
||||
|
||||
if (dst_curve == dst_curves.last()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const PointTransferData &next_dst_point =
|
||||
dst_transfer_data[dst_points_by_curve[dst_curve + 1].first()];
|
||||
|
||||
if (next_dst_point.is_cut) {
|
||||
dst_end_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dst_start_caps.finish();
|
||||
dst_end_caps.finish();
|
||||
}
|
||||
|
||||
/* Copy/Interpolate point attributes. */
|
||||
for (bke::AttributeTransferData &attribute : bke::retrieve_attributes_for_transfer(
|
||||
src_attributes, dst_attributes, ATTR_DOMAIN_MASK_POINT, propagation_info))
|
||||
{
|
||||
bke::attribute_math::convert_to_static_type(attribute.dst.span.type(), [&](auto dummy) {
|
||||
using T = decltype(dummy);
|
||||
auto src_attr = attribute.src.typed<T>();
|
||||
auto dst_attr = attribute.dst.span.typed<T>();
|
||||
|
||||
threading::parallel_for(dst.points_range(), 4096, [&](const IndexRange dst_points_range) {
|
||||
for (const int dst_point : dst_points_range) {
|
||||
const PointTransferData &point_transfer = dst_transfer_data[dst_point];
|
||||
if (point_transfer.is_src_point) {
|
||||
dst_attr[dst_point] = src_attr[point_transfer.src_point];
|
||||
}
|
||||
else {
|
||||
dst_attr[dst_point] = bke::attribute_math::mix2<T>(
|
||||
point_transfer.factor,
|
||||
src_attr[point_transfer.src_point],
|
||||
src_attr[point_transfer.src_next_point]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
attribute.dst.finish();
|
||||
});
|
||||
}
|
||||
|
||||
return dst_transfer_data;
|
||||
}
|
||||
|
||||
Vector<FalloffSample> compute_piecewise_linear_falloff() const
|
||||
{
|
||||
/* The changes in opacity implied by the soft eraser are described by a falloff curve mapping.
|
||||
@ -1256,7 +1045,7 @@ struct EraseOperationExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
const Array<PointTransferData> dst_points = recompute_topology(
|
||||
const Array<PointTransferData> dst_points = compute_topology_change(
|
||||
src, dst, src_to_dst_points, keep_caps);
|
||||
|
||||
/* Set opacity. */
|
||||
|
Loading…
Reference in New Issue
Block a user