Grease Pencil: Export other curve types to SVG
This adds support for bézier, NURBS and catmull rom curve types in SVG exporting. Note: strokes without uniform width will still be exported as polylines. This is because the outline code currently does not support bézier curves. Pull Request: https://projects.blender.org/blender/blender/pulls/141594
This commit is contained in:
committed by
Falk David
parent
8c3e81bd9b
commit
b056d3c85c
@@ -37,6 +37,7 @@
|
||||
|
||||
#include "grease_pencil_io_intern.hh"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
|
||||
@@ -390,6 +391,9 @@ void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
|
||||
"end_cap", bke::AttrDomain::Curve, 0);
|
||||
/* Point attributes. */
|
||||
const Span<float3> positions = curves.positions();
|
||||
const Span<float3> positions_left = curves.handle_positions_left();
|
||||
const Span<float3> positions_right = curves.handle_positions_right();
|
||||
const VArray<int8_t> types = curves.curve_types();
|
||||
const VArraySpan<float> radii = drawing.radii();
|
||||
const VArraySpan<float> opacities = drawing.opacities();
|
||||
const VArraySpan<ColorGeometry4f> vertex_colors = drawing.vertex_colors();
|
||||
@@ -403,6 +407,7 @@ void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
|
||||
|
||||
for (const int i_curve : curves.curves_range()) {
|
||||
const IndexRange points = points_by_curve[i_curve];
|
||||
const int8_t type = types[i_curve];
|
||||
if (points.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
@@ -432,7 +437,10 @@ void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
|
||||
const ColorGeometry4f fill_color = math::interpolate(
|
||||
material_fill_color, fill_colors[i_curve], fill_colors[i_curve].a);
|
||||
stroke_fn(positions.slice(points),
|
||||
positions_left.slice_safe(points),
|
||||
positions_right.slice_safe(points),
|
||||
is_cyclic,
|
||||
type,
|
||||
fill_color,
|
||||
layer.opacity,
|
||||
std::nullopt,
|
||||
@@ -459,7 +467,10 @@ void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
|
||||
end_cap == GP_STROKE_CAP_TYPE_ROUND;
|
||||
|
||||
stroke_fn(positions.slice(points),
|
||||
positions_left.slice_safe(points),
|
||||
positions_right.slice_safe(points),
|
||||
is_cyclic,
|
||||
type,
|
||||
stroke_color,
|
||||
stroke_opacity,
|
||||
uniform_width,
|
||||
@@ -490,12 +501,17 @@ void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
|
||||
|
||||
const OffsetIndices outline_points_by_curve = outline.points_by_curve();
|
||||
const Span<float3> outline_positions = outline.positions();
|
||||
const Span<float3> outline_positions_left = curves.handle_positions_left();
|
||||
const Span<float3> outline_positions_right = curves.handle_positions_right();
|
||||
|
||||
for (const int i_outline_curve : outline.curves_range()) {
|
||||
const IndexRange outline_points = outline_points_by_curve[i_outline_curve];
|
||||
/* Use stroke color to fill the outline. */
|
||||
stroke_fn(outline_positions.slice(outline_points),
|
||||
outline_positions_left.slice_safe(outline_points),
|
||||
outline_positions_right.slice_safe(outline_points),
|
||||
true,
|
||||
type,
|
||||
stroke_color,
|
||||
stroke_opacity,
|
||||
std::nullopt,
|
||||
@@ -547,4 +563,15 @@ bool GreasePencilExporter::is_selected_frame(const GreasePencil &grease_pencil,
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GreasePencilExporter::coord_to_svg_string(const float2 &screen_co) const
|
||||
{
|
||||
/* SVG has inverted Y axis. */
|
||||
if (camera_persmat_) {
|
||||
return fmt::format("{},{}", screen_co.x, camera_rect_.size().y - screen_co.y);
|
||||
}
|
||||
else {
|
||||
return fmt::format("{},{}", screen_co.x, screen_rect_.size().y - screen_co.y);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::grease_pencil
|
||||
|
||||
@@ -144,7 +144,10 @@ void PDFExporter::export_grease_pencil_layer(const Object &object,
|
||||
const float4x4 layer_to_world = layer.to_world_space(object);
|
||||
|
||||
auto write_stroke = [&](const Span<float3> positions,
|
||||
const Span<float3> /*positions_left*/,
|
||||
const Span<float3> /*positions_right*/,
|
||||
const bool cyclic,
|
||||
const int8_t /*type*/,
|
||||
const ColorGeometry4f &color,
|
||||
const float opacity,
|
||||
const std::optional<float> width,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
#include "GEO_resample_curves.hh"
|
||||
#include "GEO_set_curve_type.hh"
|
||||
|
||||
#include "grease_pencil_io_intern.hh"
|
||||
|
||||
@@ -129,6 +130,12 @@ class SVGExporter : public GreasePencilExporter {
|
||||
const float4x4 &transform,
|
||||
Span<float3> positions,
|
||||
bool cyclic);
|
||||
pugi::xml_node write_bezier_path(pugi::xml_node node,
|
||||
const float4x4 &transform,
|
||||
Span<float3> positions,
|
||||
Span<float3> positions_left,
|
||||
Span<float3> positions_right,
|
||||
bool cyclic);
|
||||
|
||||
bool write_to_file(StringRefNull filepath);
|
||||
};
|
||||
@@ -272,20 +279,22 @@ void SVGExporter::export_grease_pencil_objects(pugi::xml_node node, const int fr
|
||||
layer_node.append_attribute("id").set_value(layer_node_id.c_str());
|
||||
|
||||
const bke::CurvesGeometry &curves = drawing->strokes();
|
||||
/* TODO: Instead of converting all the other curve types to poly curves, export them directly
|
||||
* as curve paths to the SVG. */
|
||||
if (curves.has_curve_with_type(
|
||||
{CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_BEZIER, CURVE_TYPE_NURBS}))
|
||||
{
|
||||
/* Convert NURBS and Catmull Rom to bezier then export. */
|
||||
if (curves.has_curve_with_type({CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_NURBS})) {
|
||||
IndexMaskMemory memory;
|
||||
const IndexMask non_poly_selection = curves.indices_for_curve_type(CURVE_TYPE_POLY, memory)
|
||||
.complement(curves.curves_range(), memory);
|
||||
|
||||
Drawing export_drawing;
|
||||
export_drawing.strokes_for_write() = geometry::resample_to_evaluated(curves,
|
||||
non_poly_selection);
|
||||
export_drawing.tag_topology_changed();
|
||||
geometry::ConvertCurvesOptions options;
|
||||
options.convert_bezier_handles_to_poly_points = false;
|
||||
options.convert_bezier_handles_to_catmull_rom_points = false;
|
||||
options.keep_bezier_shape_as_nurbs = true;
|
||||
options.keep_catmull_rom_shape_as_nurbs = true;
|
||||
|
||||
Drawing export_drawing;
|
||||
export_drawing.strokes_for_write() = geometry::convert_curves(
|
||||
curves, non_poly_selection, CURVE_TYPE_BEZIER, {}, options);
|
||||
export_drawing.tag_topology_changed();
|
||||
export_grease_pencil_layer(layer_node, *ob_eval, *layer, export_drawing);
|
||||
}
|
||||
else {
|
||||
@@ -305,7 +314,10 @@ void SVGExporter::export_grease_pencil_layer(pugi::xml_node layer_node,
|
||||
const float4x4 layer_to_world = layer.to_world_space(object);
|
||||
|
||||
auto write_stroke = [&](const Span<float3> positions,
|
||||
const Span<float3> positions_left,
|
||||
const Span<float3> positions_right,
|
||||
const bool cyclic,
|
||||
const int8_t type,
|
||||
const ColorGeometry4f &color,
|
||||
const float opacity,
|
||||
const std::optional<float> width,
|
||||
@@ -316,10 +328,16 @@ void SVGExporter::export_grease_pencil_layer(pugi::xml_node layer_node,
|
||||
write_fill_color_attribute(element_node, color, opacity);
|
||||
}
|
||||
else {
|
||||
/* Fill is always exported as polygon because the stroke of the fill is done
|
||||
* in a different SVG command. */
|
||||
pugi::xml_node element_node = write_polyline(
|
||||
layer_node, layer_to_world, positions, cyclic, width);
|
||||
pugi::xml_node element_node;
|
||||
if (type == CURVE_TYPE_BEZIER) {
|
||||
element_node = write_bezier_path(
|
||||
layer_node, layer_to_world, positions, positions_left, positions_right, cyclic);
|
||||
}
|
||||
else {
|
||||
/* Fill is always exported as polygon because the stroke of the fill is done
|
||||
* in a different SVG command. */
|
||||
element_node = write_polyline(layer_node, layer_to_world, positions, cyclic, width);
|
||||
}
|
||||
|
||||
if (width) {
|
||||
write_stroke_color_attribute(element_node, color, opacity, round_cap);
|
||||
@@ -417,19 +435,13 @@ pugi::xml_node SVGExporter::write_polygon(pugi::xml_node node,
|
||||
|
||||
std::string txt;
|
||||
for (const int i : positions.index_range()) {
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
|
||||
if (i > 0) {
|
||||
txt.append(" ");
|
||||
}
|
||||
/* SVG has inverted Y axis. */
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
if (camera_persmat_) {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(camera_rect_.size().y - screen_co.y));
|
||||
}
|
||||
else {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(screen_rect_.size().y - screen_co.y));
|
||||
}
|
||||
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
}
|
||||
|
||||
element_node.append_attribute("points").set_value(txt.c_str());
|
||||
@@ -451,19 +463,13 @@ pugi::xml_node SVGExporter::write_polyline(pugi::xml_node node,
|
||||
|
||||
std::string txt;
|
||||
for (const int i : positions.index_range()) {
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
|
||||
if (i > 0) {
|
||||
txt.append(" ");
|
||||
}
|
||||
/* SVG has inverted Y axis. */
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
if (camera_persmat_) {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(camera_rect_.size().y - screen_co.y));
|
||||
}
|
||||
else {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(screen_rect_.size().y - screen_co.y));
|
||||
}
|
||||
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
}
|
||||
|
||||
element_node.append_attribute("points").set_value(txt.c_str());
|
||||
@@ -480,19 +486,13 @@ pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
|
||||
|
||||
std::string txt = "M";
|
||||
for (const int i : positions.index_range()) {
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
|
||||
if (i > 0) {
|
||||
txt.append("L");
|
||||
}
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
/* SVG has inverted Y axis. */
|
||||
if (camera_persmat_) {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(camera_rect_.size().y - screen_co.y));
|
||||
}
|
||||
else {
|
||||
txt.append(std::to_string(screen_co.x) + "," +
|
||||
std::to_string(screen_rect_.size().y - screen_co.y));
|
||||
}
|
||||
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
}
|
||||
/* Close patch (cyclic). */
|
||||
if (cyclic) {
|
||||
@@ -504,6 +504,58 @@ pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
|
||||
return element_node;
|
||||
}
|
||||
|
||||
pugi::xml_node SVGExporter::write_bezier_path(pugi::xml_node node,
|
||||
const float4x4 &transform,
|
||||
const Span<float3> positions,
|
||||
const Span<float3> positions_left,
|
||||
const Span<float3> positions_right,
|
||||
const bool cyclic)
|
||||
{
|
||||
pugi::xml_node element_node = node.append_child("path");
|
||||
|
||||
std::string txt = "M";
|
||||
for (const int i : positions.index_range().drop_back(1)) {
|
||||
const float2 screen_co = this->project_to_screen(transform, positions[i]);
|
||||
const float2 screen_co_right = this->project_to_screen(transform, positions_right[i]);
|
||||
const float2 screen_co_left = this->project_to_screen(transform, positions_left[i + 1]);
|
||||
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
txt.append(" C ");
|
||||
txt.append(coord_to_svg_string(screen_co_right));
|
||||
txt.append(", ");
|
||||
txt.append(coord_to_svg_string(screen_co_left));
|
||||
|
||||
if (i != positions.size() - 2) {
|
||||
txt.append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
txt.append(", ");
|
||||
const float2 screen_co = this->project_to_screen(transform, positions.last());
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
}
|
||||
|
||||
/* Close patch (cyclic). */
|
||||
if (cyclic) {
|
||||
const float2 screen_co_right = this->project_to_screen(transform, positions_right.last());
|
||||
const float2 screen_co_left = this->project_to_screen(transform, positions_left.first());
|
||||
const float2 screen_co = this->project_to_screen(transform, positions.first());
|
||||
|
||||
txt.append(" C ");
|
||||
txt.append(coord_to_svg_string(screen_co_right));
|
||||
txt.append(", ");
|
||||
txt.append(coord_to_svg_string(screen_co_left));
|
||||
txt.append(", ");
|
||||
txt.append(coord_to_svg_string(screen_co));
|
||||
txt.append("z");
|
||||
}
|
||||
|
||||
element_node.append_attribute("d").set_value(txt.c_str());
|
||||
|
||||
return element_node;
|
||||
}
|
||||
|
||||
bool SVGExporter::write_to_file(StringRefNull filepath)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
@@ -79,7 +79,10 @@ class GreasePencilExporter {
|
||||
Vector<ObjectInfo> retrieve_objects() const;
|
||||
|
||||
using WriteStrokeFn = FunctionRef<void(const Span<float3> positions,
|
||||
const Span<float3> positions_left,
|
||||
const Span<float3> positions_right,
|
||||
bool cyclic,
|
||||
int8_t type,
|
||||
const ColorGeometry4f &color,
|
||||
float opacity,
|
||||
std::optional<float> width,
|
||||
@@ -95,6 +98,8 @@ class GreasePencilExporter {
|
||||
|
||||
bool is_selected_frame(const GreasePencil &grease_pencil, int frame_number) const;
|
||||
|
||||
std::string coord_to_svg_string(const float2 &screen_co) const;
|
||||
|
||||
private:
|
||||
std::optional<Bounds<float2>> compute_screen_space_drawing_bounds(
|
||||
const RegionView3D &rv3d,
|
||||
|
||||
Reference in New Issue
Block a user