Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
../common
../../editors/include
../../makesdna
../../makesrna
../../../../intern/guardedalloc
../../../../intern/utfconv
)
set(INC_SYS
)
set(SRC
intern/grease_pencil_io.cc
intern/grease_pencil_io_import_svg.cc
grease_pencil_io.hh
intern/grease_pencil_io_intern.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blenloader
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::extern::nanosvg
PRIVATE bf::functions
PRIVATE bf::geometry
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::windowmanager
PRIVATE bf::dependencies::optional::pugixml
PRIVATE bf::dependencies::optional::haru
bf_io_common
)
if(WITH_PUGIXML)
list(APPEND SRC
intern/grease_pencil_io_export_svg.cc
)
endif()
if(WITH_HARU)
list(APPEND SRC
intern/grease_pencil_io_export_pdf.cc
)
endif()
blender_add_lib(bf_io_grease_pencil "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@@ -0,0 +1,97 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_string_ref.hh"
#include "DNA_view3d_types.h"
namespace blender {
#pragma once
/** \file
* \ingroup bgrease_pencil
*/
struct ARegion;
struct Depsgraph;
struct View3D;
struct bContext;
struct Scene;
struct ReportList;
namespace io::grease_pencil {
struct IOContext {
ReportList *reports;
bContext &C;
const ARegion *region;
const View3D *v3d;
const RegionView3D *rv3d;
Scene *scene;
Depsgraph *depsgraph;
IOContext(bContext &C,
const ARegion *region,
const View3D *v3d,
const RegionView3D *rv3d,
ReportList *reports);
};
struct ImportParams {
float scale = 1.0f;
int frame_number = 1;
int resolution = 10;
bool use_scene_unit = false;
bool recenter_bounds = false;
};
enum class ExportStatus : int8_t {
Ok = 0,
NoFramesSelected,
InvalidActiveObjectType,
FileWriteError,
UnknownError,
};
struct ExportParams {
/* Object to be exported. */
enum class SelectMode {
Active = 0,
Selected = 1,
Visible = 2,
};
/** Frame-range to be exported. */
enum class FrameMode {
Active = 0,
Selected = 1,
Scene = 2,
};
Object *object = nullptr;
SelectMode select_mode = SelectMode::Active;
FrameMode frame_mode = FrameMode::Active;
bool export_stroke_materials = true;
bool export_fill_materials = true;
/* Clip drawings to camera size when exporting in camera view. */
bool use_clip_camera = false;
/* Enforce uniform stroke width by averaging radius. */
bool use_uniform_width = false;
/* Distance for resampling outline curves before export, disabled if zero. */
float outline_resample_length = 0.0f;
};
bool import_svg(const IOContext &context, const ImportParams &params, StringRefNull filepath);
ExportStatus export_svg(const IOContext &context,
const ExportParams &params,
Scene &scene,
StringRefNull filepath);
bool export_pdf(const IOContext &context,
const ExportParams &params,
Scene &scene,
StringRefNull filepath);
} // namespace io::grease_pencil
} // namespace blender

View File

@@ -0,0 +1,647 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "BLI_color_types.hh"
#include "BLI_listbase.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BKE_attribute.hh"
#include "BKE_camera.h"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_layer.hh"
#include "BKE_material.hh"
#include "BKE_scene.hh"
#include "DNA_grease_pencil_types.h"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_view3d_types.h"
#include "DEG_depsgraph_query.hh"
#include "GEO_resample_curves.hh"
#include "ED_grease_pencil.hh"
#include "ED_object.hh"
#include "ED_view3d.hh"
#include "UI_view2d.hh"
#include "grease_pencil_io_intern.hh"
#include <fmt/format.h>
#include <numeric>
#include <optional>
/** \file
* \ingroup bgrease_pencil
*/
namespace blender::io::grease_pencil {
static float get_average(const Span<float> values)
{
return values.is_empty() ? 0.0f :
std::accumulate(values.begin(), values.end(), 0.0f) / values.size();
}
static ColorGeometry4f get_average(const Span<ColorGeometry4f> values)
{
if (values.is_empty()) {
return ColorGeometry4f(nullptr);
}
/* ColorGeometry4f does not support arithmetic directly. */
Span<float4> rgba_values = values.cast<float4>();
float4 avg_rgba = std::accumulate(rgba_values.begin(), rgba_values.end(), float4(0)) /
values.size();
return ColorGeometry4f(avg_rgba);
}
IOContext::IOContext(bContext &C,
const ARegion *region,
const View3D *v3d,
const RegionView3D *rv3d,
ReportList *reports)
: reports(reports),
C(C),
region(region),
v3d(v3d),
rv3d(rv3d),
scene(CTX_data_scene(&C)),
depsgraph(CTX_data_depsgraph_pointer(&C))
{
}
GreasePencilImporter::GreasePencilImporter(const IOContext &context, const ImportParams &params)
: context_(context), params_(params)
{
}
Object *GreasePencilImporter::create_object(const StringRefNull name)
{
const float3 cur_loc = context_.scene->cursor.location;
const float3 rot = float3(0.0f);
const ushort local_view_bits = (context_.v3d && context_.v3d->localvd) ?
context_.v3d->local_view_uid :
ushort(0);
Object *ob_gpencil = ed::object::add_type(
&context_.C, OB_GREASE_PENCIL, name.c_str(), cur_loc, rot, false, local_view_bits);
return ob_gpencil;
}
int GreasePencilImporter::create_material(const StringRefNull name)
{
const ColorGeometry4f default_stroke_color = {0.0f, 0.0f, 0.0f, 1.0f};
const ColorGeometry4f default_fill_color = {0.5f, 0.5f, 0.5f, 1.0f};
int mat_index = BKE_grease_pencil_object_material_index_get_by_name(object_, name.c_str());
/* Stroke and Fill material. */
if (mat_index == -1) {
Main *bmain = CTX_data_main(&context_.C);
int new_idx;
Material *mat_gp = BKE_grease_pencil_object_material_new(
bmain, object_, name.c_str(), &new_idx);
MaterialGPencilStyle *gp_style = mat_gp->gp_style;
copy_v4_v4(gp_style->stroke_rgba, default_stroke_color);
copy_v4_v4(gp_style->fill_rgba, default_fill_color);
mat_index = object_->totcol - 1;
}
return mat_index;
}
GreasePencilExporter::GreasePencilExporter(const IOContext &context, const ExportParams &params)
: context_(context), params_(params)
{
}
std::optional<Bounds<float2>> GreasePencilExporter::compute_screen_space_drawing_bounds(
const RegionView3D &rv3d,
Object &object,
const int layer_index,
const bke::greasepencil::Drawing &drawing)
{
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
std::optional<Bounds<float2>> drawing_bounds = std::nullopt;
BLI_assert(object.type == OB_GREASE_PENCIL);
GreasePencil &grease_pencil = *id_cast<GreasePencil *>(object.data);
const Layer &layer = *grease_pencil.layers()[layer_index];
const float4x4 layer_to_world = layer.to_world_space(object);
const VArray<float> radii = drawing.radii();
const bke::CurvesGeometry &strokes = drawing.strokes();
const Span<float3> positions = strokes.positions();
IndexMaskMemory memory;
const IndexMask visible_strokes = ed::greasepencil::retrieve_visible_strokes(
object, drawing, memory);
visible_strokes.foreach_index(
[&](const int curve_i) {
const IndexRange points = strokes.points_by_curve()[curve_i];
for (const int point_i : points) {
const float2 screen_co = this->project_to_screen(layer_to_world, positions[point_i]);
if (screen_co.x != V2D_IS_CLIPPED) {
const float3 world_pos = math::transform_point(layer_to_world, positions[point_i]);
const float pixels = radii[point_i] / ED_view3d_pixel_size(&rv3d, world_pos);
std::optional<Bounds<float2>> point_bounds = Bounds<float2>(screen_co);
point_bounds->pad(pixels);
drawing_bounds = bounds::merge(drawing_bounds, point_bounds);
}
}
},
exec_mode::grain_size(512));
return drawing_bounds;
}
std::optional<Bounds<float2>> GreasePencilExporter::compute_objects_bounds(
const RegionView3D &rv3d,
const Depsgraph &depsgraph,
const Span<GreasePencilExporter::ObjectInfo> objects,
const int frame_number)
{
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
using ObjectInfo = GreasePencilExporter::ObjectInfo;
constexpr float gap = 10.0f;
std::optional<Bounds<float2>> full_bounds = std::nullopt;
for (const ObjectInfo &info : objects) {
Object *object_eval = DEG_get_evaluated(&depsgraph, info.object);
const GreasePencil &grease_pencil_eval = *id_cast<GreasePencil *>(object_eval->data);
for (const int layer_index : grease_pencil_eval.layers().index_range()) {
const Layer &layer = *grease_pencil_eval.layers()[layer_index];
const Drawing *drawing = grease_pencil_eval.get_drawing_at(layer, frame_number);
if (drawing == nullptr) {
continue;
}
std::optional<Bounds<float2>> layer_bounds = this->compute_screen_space_drawing_bounds(
rv3d, *object_eval, layer_index, *drawing);
full_bounds = bounds::merge(full_bounds, layer_bounds);
}
}
/* Add small gap. */
if (full_bounds) {
full_bounds->pad(gap);
}
return full_bounds;
}
static float4x4 persmat_from_camera_object(Scene &scene)
{
/* Ensure camera switch is applied. */
BKE_scene_camera_switch_update(&scene);
/* Calculate camera matrix. */
Object *cam_ob = scene.camera;
if (cam_ob == nullptr) {
/* XXX not sure when this could ever happen if v3d camera is not null,
* conditions are from GPv2 and not explained anywhere. */
return float4x4::identity();
}
/* Set up parameters. */
CameraParams params;
BKE_camera_params_init(&params);
BKE_camera_params_from_object(&params, cam_ob);
/* Compute matrix, view-plane, etc. */
BKE_camera_params_compute_viewplane(
&params, scene.r.xsch, scene.r.ysch, scene.r.xasp, scene.r.yasp);
BKE_camera_params_compute_matrix(&params);
float4x4 viewmat = math::invert(cam_ob->object_to_world());
return float4x4(params.winmat) * viewmat;
}
void GreasePencilExporter::prepare_render_params(Scene &scene, const int frame_number)
{
const bool use_camera_view = (context_.rv3d->persp == RV3D_CAMOB) &&
(context_.v3d->camera != nullptr);
if (use_camera_view) {
/* Camera rectangle (in screen space). */
rctf camera_rect;
ED_view3d_calc_camera_border(&scene,
context_.depsgraph,
context_.region,
context_.v3d,
context_.rv3d,
true,
&camera_rect);
screen_rect_ = {{camera_rect.xmin, camera_rect.ymin}, {camera_rect.xmax, camera_rect.ymax}};
camera_persmat_ = persmat_from_camera_object(scene);
/* Output resolution (when in camera view). */
int width, height;
BKE_render_resolution(&scene.r, false, &width, &height);
camera_rect_ = {{0.0f, 0.0f}, {float(width), float(height)}};
/* Compute factor that remaps screen_rect to final output resolution. */
BLI_assert(screen_rect_.size() != float2(0.0f));
camera_fac_ = float2(camera_rect_.size()) / float2(screen_rect_.size());
}
else {
Vector<ObjectInfo> objects = this->retrieve_objects();
std::optional<Bounds<float2>> full_bounds = this->compute_objects_bounds(
*context_.rv3d, *context_.depsgraph, objects, frame_number);
screen_rect_ = full_bounds ? *full_bounds : Bounds<float2>(float2(0.0f));
camera_persmat_ = std::nullopt;
}
}
ColorGeometry4f GreasePencilExporter::compute_average_stroke_color(
const Material &material, const Span<ColorGeometry4f> vertex_colors)
{
const MaterialGPencilStyle &gp_style = *material.gp_style;
const ColorGeometry4f material_color = ColorGeometry4f(gp_style.stroke_rgba);
const ColorGeometry4f avg_vertex_color = get_average(vertex_colors);
return math::interpolate(material_color, avg_vertex_color, avg_vertex_color.a);
}
float GreasePencilExporter::compute_average_stroke_opacity(const Span<float> opacities)
{
return get_average(opacities);
}
std::optional<float> GreasePencilExporter::try_get_uniform_point_width(
const RegionView3D &rv3d, const Span<float3> world_positions, const Span<float> radii)
{
if (world_positions.is_empty()) {
return std::nullopt;
}
BLI_assert(world_positions.size() == radii.size());
Array<float> widths(world_positions.size());
threading::parallel_for(widths.index_range(), 4096, [&](const IndexRange range) {
for (const int index : range) {
const float3 &pos = world_positions[index];
const float radius = radii[index];
/* Compute the width in screen space by dividing by the pixel size at the point position. */
widths[index] = 2.0f * radius / ED_view3d_pixel_size(&rv3d, pos);
}
});
return get_average(widths);
}
Vector<GreasePencilExporter::ObjectInfo> GreasePencilExporter::retrieve_objects() const
{
using SelectMode = ExportParams::SelectMode;
const Main *bmain = CTX_data_main(&context_.C);
Scene &scene = *CTX_data_scene(&context_.C);
ViewLayer *view_layer = CTX_data_view_layer(&context_.C);
const float3 camera_z_axis = float3(context_.rv3d->viewinv[2]);
BKE_view_layer_synced_ensure(*bmain, &scene, view_layer);
Vector<ObjectInfo> objects;
auto add_object = [&](Object *object) {
if (object == nullptr || object->type != OB_GREASE_PENCIL) {
return;
}
const float3 position = object->object_to_world().location();
/* Save z-depth from view to sort from back to front. */
const bool use_ortho_depth = camera_persmat_ || !context_.rv3d->is_persp;
const float depth = use_ortho_depth ? math::dot(camera_z_axis, position) :
-ED_view3d_calc_zfac(context_.rv3d, position);
objects.append({object, depth});
};
switch (params_.select_mode) {
case SelectMode::Active:
add_object(params_.object);
break;
case SelectMode::Selected:
for (Base &base : *BKE_view_layer_object_bases_get(view_layer)) {
if (base.flag & BASE_SELECTED) {
add_object(base.object);
}
}
break;
case SelectMode::Visible:
for (Base &base : *BKE_view_layer_object_bases_get(view_layer)) {
if ((base.flag & BASE_ENABLED_RENDER) != 0) {
add_object(base.object);
}
}
break;
}
/* Sort list of objects from point of view. */
std::ranges::sort(objects, [](const ObjectInfo &info1, const ObjectInfo &info2) {
return info1.depth < info2.depth;
});
return objects;
}
static float get_miter_limit_angle(const VArray<float> miter_angles,
const IndexRange points,
const bool is_cyclic)
{
/* Because the SVG file format only supports `linejoin` type per stroke. We use priority
* system to decide what type to use.
* The order from lowest to highest is `Round`, `Bevel` then `Miter` */
float miter_limit_angle = GP_STROKE_MITER_ANGLE_ROUND;
/* Don't check the ends unless cyclical. */
for (const int point_i : points.drop_back(is_cyclic ? 0 : 1).drop_front(is_cyclic ? 0 : 1)) {
const float point_miter_angle = miter_angles[point_i];
/* Miter should take priority over Round. */
if (point_miter_angle <= GP_STROKE_MITER_ANGLE_ROUND) {
continue;
}
/* This point's limit should replace the round type. */
if (miter_limit_angle <= GP_STROKE_MITER_ANGLE_ROUND) {
miter_limit_angle = point_miter_angle;
}
/* Sharp corners (Lower angles) should take priority. */
miter_limit_angle = math::min(miter_limit_angle, point_miter_angle);
}
return miter_limit_angle;
}
void GreasePencilExporter::foreach_shape_in_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing,
WriteShapeFn shape_fn)
{
using bke::greasepencil::Drawing;
const float4x4 layer_to_world = layer.to_world_space(object);
const float4x4 viewmat = float4x4(context_.rv3d->viewmat);
const float4x4 layer_to_view = viewmat * layer_to_world;
const bke::CurvesGeometry &curves = drawing.strokes();
const bke::AttributeAccessor attributes = curves.attributes();
/* Curve attributes. */
const OffsetIndices points_by_curve = curves.points_by_curve();
const VArray<bool> cyclic = curves.cyclic();
const VArraySpan<int> material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Curve, 0);
const VArraySpan<ColorGeometry4f> fill_colors = drawing.fill_colors();
const VArray<int8_t> start_caps = *attributes.lookup_or_default<int8_t>(
"start_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_TYPE_ROUND);
const VArray<int8_t> end_caps = *attributes.lookup_or_default<int8_t>(
"end_cap", bke::AttrDomain::Curve, 0);
const VArray<bool> hide_stroke = *attributes.lookup_or_default<bool>(
"hide_stroke", bke::AttrDomain::Curve, false);
const VArray<int> fill_ids = *attributes.lookup_or_default<int>(
"fill_id", bke::AttrDomain::Curve, 0);
const VArray<float> miter_angles = *attributes.lookup_or_default<float>(
"miter_angle", bke::AttrDomain::Point, GP_STROKE_MITER_ANGLE_ROUND);
/* 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 std::optional<GroupedSpan<int>> fills = drawing.fills();
const VArraySpan<float> radii = drawing.radii();
const VArraySpan<float> opacities = drawing.opacities();
const VArraySpan<ColorGeometry4f> vertex_colors = drawing.vertex_colors();
Array<float3> world_positions(positions.size());
math::transform_points(positions, layer_to_world, world_positions);
/* Fills are made of multiple curves. Keep track of which curve is part of which fill. */
Array<int> fill_index_by_curves(curves.curves_num(), -1);
/* Keep track of which curve is the first in a fill (e.g. the same index is used for each curve
* in the same fill). */
Array<int> first_curves(curves.curves_num());
array_utils::fill_index_range<int>(first_curves);
int fill_index = 0;
for (const int i_curve : curves.curves_range()) {
const bool is_filled = fill_ids[i_curve] != 0;
const bool active_filled = is_filled && (fill_index_by_curves[i_curve] == -1);
/* Keep track of already rendered fills. */
if (active_filled) {
const Span<int> fill = (*fills)[fill_index];
const int first_curve = fill.first();
for (const int pos : fill.index_range()) {
const int i_curve = fill[pos];
fill_index_by_curves[i_curve] = fill_index;
first_curves[i_curve] = first_curve;
}
fill_index++;
}
}
/* Iterate over all the curves and render the strokes (if shown). For fills, make sure that they
* are rendered when the first curve of the fill is encountered and don't re-render the same fill
* multiple times. */
for (const int i_curve : curves.curves_range()) {
/* Will be `-1` if not a fill. */
const int fill_index = fill_index_by_curves[i_curve];
const bool is_filled = fill_index != -1;
const bool active_filled = is_filled && (first_curves[i_curve] == i_curve);
const int material_index = material_indices[i_curve];
const Material *material = [&]() {
const Material *material = BKE_object_material_get(const_cast<Object *>(&object),
material_index + 1);
if (!material) {
const Material *material_default = BKE_material_default_gpencil();
return material_default;
}
return material;
}();
BLI_assert(material->gp_style != nullptr);
if (material->gp_style->flag & GP_MATERIAL_HIDE) {
continue;
}
/* Fill. */
if (active_filled && params_.export_fill_materials) {
const Span<int> fill = (*fills)[fill_index];
const ColorGeometry4f material_fill_color = ColorGeometry4f(material->gp_style->fill_rgba);
const ColorGeometry4f fill_color = math::interpolate(
material_fill_color, fill_colors[i_curve], fill_colors[i_curve].a);
shape_fn(positions,
positions_left,
positions_right,
points_by_curve,
fill,
cyclic,
types,
fill_color,
layer.opacity,
std::nullopt,
std::nullopt,
false,
false);
}
/* Stroke. */
if (!hide_stroke[i_curve] && params_.export_stroke_materials) {
const IndexRange points = points_by_curve[i_curve];
const ColorGeometry4f stroke_color = compute_average_stroke_color(
*material, vertex_colors.slice(points));
const float stroke_opacity = compute_average_stroke_opacity(opacities.slice(points)) *
layer.opacity;
const std::optional<float> uniform_width = params_.use_uniform_width ?
try_get_uniform_point_width(
*context_.rv3d,
world_positions.as_span().slice(points),
radii.slice(points)) :
std::nullopt;
if (uniform_width) {
const bool is_cyclic = cyclic[i_curve];
const GreasePencilStrokeCapType start_cap = GreasePencilStrokeCapType(start_caps[i_curve]);
const GreasePencilStrokeCapType end_cap = GreasePencilStrokeCapType(end_caps[i_curve]);
const bool round_cap = start_cap == GP_STROKE_CAP_TYPE_ROUND ||
end_cap == GP_STROKE_CAP_TYPE_ROUND;
const float miter_limit_angle = get_miter_limit_angle(miter_angles, points, is_cyclic);
shape_fn(positions,
positions_left,
positions_right,
points_by_curve,
{i_curve},
cyclic,
types,
stroke_color,
stroke_opacity,
uniform_width,
miter_limit_angle,
round_cap,
false);
}
else {
const IndexMask single_curve_mask = IndexRange::from_single(i_curve);
constexpr int corner_subdivisions = 3;
constexpr float outline_radius = 0.0f;
constexpr float outline_offset = 0.0f;
bke::CurvesGeometry outline = ed::greasepencil::create_curves_outline(drawing,
single_curve_mask,
layer_to_view,
corner_subdivisions,
outline_radius,
outline_offset,
material_index);
/* Sample the outline stroke. */
if (params_.outline_resample_length > 0.0f) {
VArray<float> resample_lengths = VArray<float>::from_single(
params_.outline_resample_length, outline.curves_num());
outline = geometry::resample_to_length(
outline, outline.curves_range(), resample_lengths);
}
const OffsetIndices outline_points_by_curve = outline.points_by_curve();
const VArray<bool> outline_cyclic = outline.cyclic();
const Span<float3> outline_positions = outline.positions();
const Span<float3> outline_positions_left = *outline.handle_positions_left();
const Span<float3> outline_positions_right = *outline.handle_positions_right();
const VArray<int8_t> outline_types = outline.curve_types();
Array<int> outline_shape(outline.curves_num());
array_utils::fill_index_range<int>(outline_shape.as_mutable_span());
/* Use stroke color to fill the outline. */
shape_fn(outline_positions,
outline_positions_left,
outline_positions_right,
outline_points_by_curve,
outline_shape.as_span(),
outline_cyclic,
outline_types,
stroke_color,
stroke_opacity,
std::nullopt,
std::nullopt,
false,
true);
}
}
}
}
float2 GreasePencilExporter::project_to_screen(const float4x4 &transform,
const float3 &position) const
{
const float3 world_pos = math::transform_point(transform, position);
if (camera_persmat_) {
/* Use camera render space. */
const float2 cam_space = (float2(math::project_point(*camera_persmat_, world_pos)) + 1.0f) /
2.0f * float2(screen_rect_.size());
return cam_space * camera_fac_;
}
/* Use 3D view screen space. */
float2 screen_co;
if (ED_view3d_project_float_global(context_.region, world_pos, screen_co, V3D_PROJ_TEST_NOP) ==
V3D_PROJ_RET_OK)
{
if (!ELEM(V2D_IS_CLIPPED, screen_co.x, screen_co.y)) {
/* Apply offset and scale. */
return screen_co - screen_rect_.min;
}
}
return float2(V2D_IS_CLIPPED);
}
bool GreasePencilExporter::is_selected_frame(const GreasePencil &grease_pencil,
const int frame_number) const
{
for (const bke::greasepencil::Layer *layer : grease_pencil.layers()) {
if (layer->is_visible()) {
const GreasePencilFrame *frame = layer->frame_at(frame_number);
if ((frame != nullptr) && frame->is_selected()) {
return true;
}
}
}
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);
}
return fmt::format("{},{}", screen_co.x, screen_rect_.size().y - screen_co.y);
}
} // namespace blender::io::grease_pencil

View File

@@ -0,0 +1,361 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_scene.hh"
#include "BLI_math_color.h"
#include "DEG_depsgraph_query.hh"
#include "DNA_grease_pencil_types.h"
#include "DNA_scene_types.h"
#include "BKE_curves.hh"
#include "GEO_resample_curves.hh"
#include "grease_pencil_io.hh"
#include "grease_pencil_io_intern.hh"
#include "hpdf.h"
#include <iostream>
/** \file
* \ingroup bgrease_pencil
*/
namespace blender::io::grease_pencil {
class PDFExporter : public GreasePencilExporter {
public:
using GreasePencilExporter::GreasePencilExporter;
HPDF_Doc pdf_;
HPDF_Page page_;
bool export_scene(Scene &scene, StringRefNull filepath);
void export_grease_pencil_objects(int frame_number);
void export_grease_pencil_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing);
bool create_document();
bool add_page(Scene &scene);
void write_path(const float4x4 &transform,
Span<float3> positions,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
const ColorGeometry4f &color,
const float opacity,
std::optional<float> width,
std::optional<float> miter_limit_angle);
bool write_to_file(StringRefNull filepath);
};
bool PDFExporter::export_scene(Scene &scene, StringRefNull filepath)
{
bool result = false;
Object *ob_eval = DEG_get_evaluated(context_.depsgraph, params_.object);
if (!create_document()) {
return false;
}
switch (params_.frame_mode) {
case ExportParams::FrameMode::Active: {
const int frame_number = scene.r.cfra;
this->prepare_render_params(scene, frame_number);
this->add_page(scene);
this->export_grease_pencil_objects(frame_number);
result = this->write_to_file(filepath);
break;
}
case ExportParams::FrameMode::Selected: {
case ExportParams::FrameMode::Scene:
const bool only_selected = (params_.frame_mode == ExportParams::FrameMode::Selected);
if (only_selected && (!ob_eval || ob_eval->type != OB_GREASE_PENCIL)) {
/* For exporting "Selected Frames", the active object is required to be a grease pencil
* object, from which we will read selected frames from. */
break;
}
const int orig_frame = scene.r.cfra;
for (int frame_number = scene.r.sfra; frame_number <= scene.r.efra; frame_number++) {
if (only_selected) {
if (!ob_eval) {
break;
}
GreasePencil &grease_pencil = *id_cast<GreasePencil *>(ob_eval->data);
if (!this->is_selected_frame(grease_pencil, frame_number)) {
continue;
}
}
scene.r.cfra = frame_number;
BKE_scene_graph_update_for_newframe(context_.depsgraph);
this->prepare_render_params(scene, frame_number);
this->add_page(scene);
this->export_grease_pencil_objects(frame_number);
}
result = this->write_to_file(filepath);
/* Back to original frame. */
scene.r.cfra = orig_frame;
BKE_scene_camera_switch_update(&scene);
BKE_scene_graph_update_for_newframe(context_.depsgraph);
break;
}
default:
break;
}
return result;
}
void PDFExporter::export_grease_pencil_objects(const int frame_number)
{
using bke::greasepencil::Drawing;
Vector<ObjectInfo> objects = retrieve_objects();
for (const ObjectInfo &info : objects) {
const Object *ob = info.object;
/* Use evaluated version to get strokes with modifiers. */
const Object *ob_eval = DEG_get_evaluated(context_.depsgraph, ob);
BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
const GreasePencil *grease_pencil_eval = id_cast<const GreasePencil *>(ob_eval->data);
for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
if (!layer->is_visible()) {
continue;
}
const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
if (drawing == nullptr) {
continue;
}
const bke::CurvesGeometry &curves = drawing->strokes();
if (curves.has_curve_with_type(
{CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_BEZIER, 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();
export_grease_pencil_layer(*ob_eval, *layer, export_drawing);
}
else {
export_grease_pencil_layer(*ob_eval, *layer, *drawing);
}
}
}
}
void PDFExporter::export_grease_pencil_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing)
{
using bke::greasepencil::Drawing;
const float4x4 layer_to_world = layer.to_world_space(object);
auto write_shape = [&](const Span<float3> positions,
const Span<float3> /*positions_left*/,
const Span<float3> /*positions_right*/,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
const ColorGeometry4f &color,
const float opacity,
const std::optional<float> width,
const std::optional<float> miter_limit_angle,
const bool /*round_cap*/,
const bool /*is_outline*/) {
write_path(layer_to_world,
positions,
points_by_curve,
shape,
cyclic,
types,
color,
opacity,
width,
miter_limit_angle);
};
foreach_shape_in_layer(object, layer, drawing, write_shape);
}
bool PDFExporter::create_document()
{
auto hpdf_error_handler = [](HPDF_STATUS error_no, HPDF_STATUS detail_no, void * /*user_data*/) {
printf("ERROR: error_no=%04X, detail_no=%u\n", (HPDF_UINT)error_no, (HPDF_UINT)detail_no);
};
pdf_ = HPDF_New(hpdf_error_handler, nullptr);
if (!pdf_) {
std::cout << "error: cannot create PdfDoc object\n";
return false;
}
return true;
}
constexpr double meter_to_inches_factor = 1000.0 / 25.4;
constexpr double default_pdf_ppi = 72.0;
bool PDFExporter::add_page(Scene &scene)
{
page_ = HPDF_AddPage(pdf_);
if (!page_) {
std::cout << "error: cannot create PdfPage\n";
return false;
}
/* Pixels per meter. */
double2 ppm;
BKE_scene_ppm_get(&scene.r, ppm);
/* Covert pixels per meter to pixels per inch. */
double2 ppi = ppm / meter_to_inches_factor;
double2 scale_factor = default_pdf_ppi / ppi;
HPDF_Page_Concat(page_, scale_factor.x, 0.0f, 0.0f, scale_factor.y, 0.0f, 0.0f);
if (camera_persmat_) {
HPDF_Page_SetWidth(page_, camera_rect_.size().x * scale_factor.x);
HPDF_Page_SetHeight(page_, camera_rect_.size().y * scale_factor.y);
}
else {
HPDF_Page_SetWidth(page_, screen_rect_.size().x * scale_factor.x);
HPDF_Page_SetHeight(page_, screen_rect_.size().y * scale_factor.y);
}
return true;
}
void PDFExporter::write_path(const float4x4 &transform,
const Span<float3> positions,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> & /*types*/,
const ColorGeometry4f &color,
const float opacity,
std::optional<float> width,
std::optional<float> miter_limit_angle)
{
if (miter_limit_angle) {
if (*miter_limit_angle <= GP_STROKE_MITER_ANGLE_ROUND) {
HPDF_Page_SetLineJoin(page_, HPDF_ROUND_JOIN);
}
else if (*miter_limit_angle >= GP_STROKE_MITER_ANGLE_BEVEL) {
HPDF_Page_SetLineJoin(page_, HPDF_BEVEL_JOIN);
}
else {
/* Convert the Miter angle to the Miter limit. */
const float miter_limit = 1.0f / math::sin(*miter_limit_angle / 2.0f);
HPDF_Page_SetLineJoin(page_, HPDF_MITER_JOIN);
HPDF_Page_SetMiterLimit(page_, miter_limit);
}
}
if (width) {
HPDF_Page_SetLineWidth(page_, std::max(*width, 1.0f));
}
const float total_opacity = color.a * opacity;
HPDF_Page_GSave(page_);
HPDF_ExtGState gstate = (total_opacity < 1.0f) ? HPDF_CreateExtGState(pdf_) : nullptr;
ColorGeometry4f srgb;
linearrgb_to_srgb_v3_v3(srgb, color);
if (width) {
HPDF_Page_SetRGBFill(page_, srgb.r, srgb.g, srgb.b);
HPDF_Page_SetRGBStroke(page_, srgb.r, srgb.g, srgb.b);
if (gstate) {
HPDF_ExtGState_SetAlphaFill(gstate, std::clamp(total_opacity, 0.0f, 1.0f));
HPDF_ExtGState_SetAlphaStroke(gstate, std::clamp(total_opacity, 0.0f, 1.0f));
}
}
else {
HPDF_Page_SetRGBFill(page_, srgb.r, srgb.g, srgb.b);
if (gstate) {
HPDF_ExtGState_SetAlphaFill(gstate, std::clamp(total_opacity, 0.0f, 1.0f));
}
}
if (gstate) {
HPDF_Page_SetExtGState(page_, gstate);
}
for (const int curve_i : shape) {
const IndexRange points = points_by_curve[curve_i];
const Span<float3> curve_pos = positions.slice(points);
for (const int i : curve_pos.index_range()) {
const float2 screen_co = this->project_to_screen(transform, curve_pos[i]);
if (i == 0) {
HPDF_Page_MoveTo(page_, screen_co.x, screen_co.y);
}
else {
HPDF_Page_LineTo(page_, screen_co.x, screen_co.y);
}
}
if (cyclic[curve_i]) {
HPDF_Page_ClosePath(page_);
}
}
if (width) {
HPDF_Page_Stroke(page_);
}
else {
HPDF_Page_Fill(page_);
}
HPDF_Page_GRestore(page_);
}
bool PDFExporter::write_to_file(StringRefNull filepath)
{
/* Support unicode character paths on Windows. */
HPDF_STATUS result = 0;
/* TODO: It looks `libharu` does not support unicode. */
#if 0 /* `ifdef WIN32` */
wchar_t *filepath_16 = alloc_utf16_from_8(filepath.c_str(), 0);
std::wstring wstr(filepath_16);
result = HPDF_SaveToFile(pdf_, wstr.c_str());
free(filepath_16);
#else
result = HPDF_SaveToFile(pdf_, filepath.c_str());
#endif
return (result == 0) ? true : false;
}
bool export_pdf(const IOContext &context,
const ExportParams &params,
Scene &scene,
StringRefNull filepath)
{
PDFExporter exporter(context, params);
return exporter.export_scene(scene, filepath);
}
} // namespace blender::io::grease_pencil

View File

@@ -0,0 +1,565 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "BLI_color_types.hh"
#include "BLI_math_color.h"
#include "BLI_string_utf8.h"
#include "BLI_vector.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_scene.hh"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DEG_depsgraph_query.hh"
#include "GEO_resample_curves.hh"
#include "GEO_set_curve_type.hh"
#include "grease_pencil_io_intern.hh"
#include <fmt/core.h>
#include <fmt/format.h>
#include <optional>
#include <pugixml.hpp>
#ifdef WIN32
# include "utfconv.hh"
#endif
/** \file
* \ingroup bgrease_pencil
*/
namespace blender::io::grease_pencil {
constexpr const char *svg_exporter_name = "SVG Export for Grease Pencil";
constexpr const char *svg_exporter_version = "v2.1";
static std::string rgb_to_hexstr(const float color[3])
{
uint8_t r = color[0] * 255.0f;
uint8_t g = color[1] * 255.0f;
uint8_t b = color[2] * 255.0f;
return fmt::format("#{:02X}{:02X}{:02X}", r, g, b);
}
static void write_stroke_color_attribute(pugi::xml_node node,
const ColorGeometry4f &stroke_color,
const float stroke_opacity,
const std::optional<float> miter_limit_angle,
const bool round_cap)
{
ColorGeometry4f color;
linearrgb_to_srgb_v3_v3(color, stroke_color);
std::string stroke_hex = rgb_to_hexstr(color);
node.append_attribute("stroke").set_value(stroke_hex.c_str());
node.append_attribute("stroke-opacity").set_value(stroke_color.a * stroke_opacity);
node.append_attribute("fill").set_value("none");
node.append_attribute("stroke-linecap").set_value(round_cap ? "round" : "square");
if (miter_limit_angle) {
if (*miter_limit_angle <= GP_STROKE_MITER_ANGLE_ROUND) {
node.append_attribute("stroke-linejoin").set_value("round");
}
else if (*miter_limit_angle >= GP_STROKE_MITER_ANGLE_BEVEL) {
node.append_attribute("stroke-linejoin").set_value("bevel");
}
else {
/* Convert the Miter angle to the Miter limit. */
const float miter_limit = 1.0f / math::sin(*miter_limit_angle / 2.0f);
node.append_attribute("stroke-linejoin").set_value("miter");
node.append_attribute("stroke-miterlimit").set_value(miter_limit);
}
}
}
static void write_fill_color_attribute(pugi::xml_node node,
const ColorGeometry4f &fill_color,
const float layer_opacity)
{
ColorGeometry4f color;
linearrgb_to_srgb_v3_v3(color, fill_color);
std::string stroke_hex = rgb_to_hexstr(color);
node.append_attribute("fill").set_value(stroke_hex.c_str());
node.append_attribute("stroke").set_value("none");
node.append_attribute("fill-opacity").set_value(fill_color.a * layer_opacity);
}
static void write_rect(pugi::xml_node node,
const float x,
const float y,
const float width,
const float height,
const float thickness,
const std::string &hexcolor)
{
pugi::xml_node rect_node = node.append_child("rect");
rect_node.append_attribute("x").set_value(x);
rect_node.append_attribute("y").set_value(y);
rect_node.append_attribute("width").set_value(width);
rect_node.append_attribute("height").set_value(height);
rect_node.append_attribute("fill").set_value("none");
if (thickness > 0.0f) {
rect_node.append_attribute("stroke").set_value(hexcolor.c_str());
rect_node.append_attribute("stroke-width").set_value(thickness);
}
}
class SVGExporter : public GreasePencilExporter {
uint64_t _node_uuid = 0;
std::string get_node_uuid_string();
public:
using GreasePencilExporter::GreasePencilExporter;
pugi::xml_document main_doc_;
ExportStatus export_scene(Scene &scene, StringRefNull filepath);
void export_grease_pencil_objects(pugi::xml_node node, int frame_number);
void export_grease_pencil_layer(pugi::xml_node node,
const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing);
void write_document_header();
pugi::xml_node write_main_node();
pugi::xml_node write_animation_node(pugi::xml_node parent_node,
IndexMask frames,
float duration);
pugi::xml_node write_path(pugi::xml_node node,
const float4x4 &transform,
const Span<float3> positions,
const Span<float3> positions_left,
const Span<float3> positions_right,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
std::optional<float> width);
bool write_to_file(StringRefNull filepath);
};
std::string SVGExporter::get_node_uuid_string()
{
std::string id = fmt::format(".uuid_{:#x}", this->_node_uuid++);
return id;
}
ExportStatus SVGExporter::export_scene(Scene &scene, StringRefNull filepath)
{
this->_node_uuid = 0;
switch (params_.frame_mode) {
case ExportParams::FrameMode::Active: {
const int frame_number = scene.r.cfra;
this->prepare_render_params(scene, frame_number);
this->write_document_header();
pugi::xml_node main_node = this->write_main_node();
this->export_grease_pencil_objects(main_node, frame_number);
const bool write_success = this->write_to_file(filepath);
return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
}
case ExportParams::FrameMode::Selected:
case ExportParams::FrameMode::Scene: {
const bool selection_only = params_.frame_mode == ExportParams::FrameMode::Selected;
const int orig_frame = scene.r.cfra;
IndexMask frames = IndexMask(IndexRange(scene.r.sfra, scene.r.efra - scene.r.sfra + 1));
IndexMaskMemory memory;
if (selection_only) {
const Object *ob_eval = DEG_get_evaluated(context_.depsgraph, params_.object);
if (!ob_eval || ob_eval->type != OB_GREASE_PENCIL) {
return ExportStatus::InvalidActiveObjectType;
}
const GreasePencil &grease_pencil = *id_cast<GreasePencil *>(ob_eval->data);
frames = IndexMask::from_predicate(frames, memory, [&](const int frame_number) {
return this->is_selected_frame(grease_pencil, frame_number);
});
}
if (frames.is_empty()) {
return ExportStatus::NoFramesSelected;
}
this->prepare_render_params(scene, frames.first());
this->write_document_header();
pugi::xml_node main_node = this->write_main_node();
/* Put frames in a hidden group. They are referenced later by a `<use>-node` that displays
* them in order. Use a group rather than a `<defs>-node` because some graphics applications
* don't expose those to users making it hard for them to work with the file.
*/
pugi::xml_node frames_group_node = main_node.append_child("g");
frames_group_node.append_attribute("id").set_value("blender_frames");
frames_group_node.append_attribute("display").set_value("none");
const int frame_count = frames.size();
const float duration = scene.r.frs_sec_base * frame_count / scene.r.frs_sec;
frames.foreach_index([&](const int frame_number) {
scene.r.cfra = frame_number;
BKE_scene_graph_update_for_newframe(context_.depsgraph);
this->prepare_render_params(scene, frame_number);
this->export_grease_pencil_objects(frames_group_node, frame_number);
});
/* Back to original frame. */
scene.r.cfra = orig_frame;
BKE_scene_camera_switch_update(&scene);
BKE_scene_graph_update_for_newframe(context_.depsgraph);
this->write_animation_node(main_node, frames, duration);
const bool write_success = this->write_to_file(filepath);
return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
}
default:
BLI_assert_unreachable();
return ExportStatus::UnknownError;
}
}
static std::string frame_name(int frame_number)
{
std::string frametxt = "blender_frame." + std::to_string(frame_number);
return frametxt;
}
void SVGExporter::export_grease_pencil_objects(pugi::xml_node node, const int frame_number)
{
using bke::greasepencil::Drawing;
const bool is_clipping = camera_persmat_ && params_.use_clip_camera;
Vector<ObjectInfo> objects = retrieve_objects();
/* Camera clipping. */
if (is_clipping) {
pugi::xml_node clip_node = node.append_child("clipPath");
clip_node.append_attribute("id").set_value(
("clip-path." + std::to_string(frame_number)).c_str());
write_rect(clip_node, 0, 0, camera_rect_.size().x, camera_rect_.size().y, 0.0f, "#000000");
}
pugi::xml_node frame_node = node.append_child("g");
frame_node.append_attribute("id").set_value(frame_name(frame_number).c_str());
/* Clip area. */
if (is_clipping) {
frame_node.append_attribute("clip-path")
.set_value(("url(#clip-path." + std::to_string(frame_number) + ")").c_str());
}
for (const ObjectInfo &info : objects) {
const Object *ob = info.object;
pugi::xml_node ob_node = frame_node.append_child("g");
char obtxt[15 + (MAX_ID_NAME - 2) + 1 + 11 + 1]; /* Final +1 for the null terminator. */
SNPRINTF_UTF8(obtxt, "blender_object.%s.%d", ob->id.name + 2, frame_number);
std::string object_id = std::string(obtxt) + this->get_node_uuid_string();
ob_node.append_attribute("id").set_value(object_id.c_str());
/* Use evaluated version to get strokes with modifiers. */
const Object *ob_eval = DEG_get_evaluated(context_.depsgraph, ob);
BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
const GreasePencil *grease_pencil_eval = id_cast<const GreasePencil *>(ob_eval->data);
for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
if (!layer->is_visible()) {
continue;
}
const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
if (drawing == nullptr) {
continue;
}
/* Layer node. */
pugi::xml_node layer_node = ob_node.append_child("g");
std::string layer_node_id = "layer." + layer->name() + this->get_node_uuid_string();
layer_node.append_attribute("id").set_value(layer_node_id.c_str());
const bke::CurvesGeometry &curves = drawing->strokes();
/* 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);
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 {
export_grease_pencil_layer(layer_node, *ob_eval, *layer, *drawing);
}
}
}
}
void SVGExporter::export_grease_pencil_layer(pugi::xml_node layer_node,
const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing)
{
using bke::greasepencil::Drawing;
const float4x4 layer_to_world = layer.to_world_space(object);
auto write_shape = [&](const Span<float3> positions,
const Span<float3> positions_left,
const Span<float3> positions_right,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
const ColorGeometry4f &color,
const float opacity,
const std::optional<float> width,
const std::optional<float> miter_limit_angle,
const bool round_cap,
const bool is_outline) {
pugi::xml_node element_node = write_path(layer_node,
layer_to_world,
positions,
positions_left,
positions_right,
points_by_curve,
shape,
cyclic,
types,
width);
if (is_outline) {
write_fill_color_attribute(element_node, color, opacity);
/* Outlines might self-overlap which creates visual holes with the `even-odd` fill rule. */
element_node.append_attribute("fill-rule").set_value("nonzero");
}
else {
element_node.append_attribute("fill-rule").set_value("evenodd");
if (width) {
write_stroke_color_attribute(element_node, color, opacity, miter_limit_angle, round_cap);
}
else {
write_fill_color_attribute(element_node, color, opacity);
}
}
};
foreach_shape_in_layer(object, layer, drawing, write_shape);
}
void SVGExporter::write_document_header()
{
/* Add a custom document declaration node. */
pugi::xml_node decl = main_doc_.prepend_child(pugi::node_declaration);
decl.append_attribute("version") = "1.0";
decl.append_attribute("encoding") = "UTF-8";
pugi::xml_node comment = main_doc_.append_child(pugi::node_comment);
std::string txt = std::string(" Generator: Blender, ") + svg_exporter_name + " - " +
svg_exporter_version + " ";
comment.set_value(txt.c_str());
pugi::xml_node doctype = main_doc_.append_child(pugi::node_doctype);
doctype.set_value(
"svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
"\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"");
}
pugi::xml_node SVGExporter::write_main_node()
{
pugi::xml_node main_node = main_doc_.append_child("svg");
main_node.append_attribute("version").set_value("1.1");
main_node.append_attribute("x").set_value("0px");
main_node.append_attribute("y").set_value("0px");
main_node.append_attribute("xmlns").set_value("http://www.w3.org/2000/svg");
std::string width, height;
if (camera_persmat_) {
width = std::to_string(camera_rect_.size().x);
height = std::to_string(camera_rect_.size().y);
}
else {
width = std::to_string(screen_rect_.size().x);
height = std::to_string(screen_rect_.size().y);
}
main_node.append_attribute("width").set_value((width + "px").c_str());
main_node.append_attribute("height").set_value((height + "px").c_str());
std::string viewbox = "0 0 " + width + " " + height;
main_node.append_attribute("viewBox").set_value(viewbox.c_str());
return main_node;
}
pugi::xml_node SVGExporter::write_animation_node(pugi::xml_node parent_node,
IndexMask frames,
const float duration)
{
pugi::xml_node use_node = parent_node.append_child("use");
use_node.append_attribute("id").set_value("blender_animation");
std::string href_text = "#" + frame_name(frames.first());
use_node.append_attribute("href").set_value(href_text.c_str());
pugi::xml_node animate_node = use_node.append_child("animate");
animate_node.append_attribute("id").set_value("frame-by-frame_animation");
animate_node.append_attribute("attributeName").set_value("href");
std::string duration_text = std::to_string(duration) + "s";
animate_node.append_attribute("dur").set_value(duration_text.c_str());
animate_node.append_attribute("repeatCount").set_value("indefinite");
std::string animated_frame_ids = [&]() {
std::string frame_ids_text = "";
frames.foreach_index([&](const int frame) {
std::string frame_url_entry = "#" + frame_name(frame) + ";";
frame_ids_text.append(frame_url_entry);
});
return frame_ids_text;
}();
animate_node.append_attribute("values").set_value(animated_frame_ids.c_str());
return use_node;
}
pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
const float4x4 &transform,
const Span<float3> positions,
const Span<float3> positions_left,
const Span<float3> positions_right,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
const std::optional<float> width)
{
pugi::xml_node element_node = node.append_child("path");
if (width) {
element_node.append_attribute("stroke-width").set_value(*width);
}
std::string txt;
for (const int curve_i : shape) {
txt.append("M");
const IndexRange points = points_by_curve[curve_i];
const Span<float3> curve_pos = positions.slice(points);
if (types[curve_i] != CURVE_TYPE_BEZIER) {
for (const int i : curve_pos.index_range()) {
const float2 screen_co = this->project_to_screen(transform, curve_pos[i]);
if (i > 0) {
txt.append("L");
}
txt.append(coord_to_svg_string(screen_co));
}
/* Close path (cyclic). */
if (cyclic[curve_i]) {
txt.append("z");
}
}
else {
const Span<float3> curve_pos_right = positions_right.slice(points);
const Span<float3> curve_pos_left = positions_left.slice(points);
for (const int i : curve_pos.index_range().drop_back(1)) {
const float2 screen_co = this->project_to_screen(transform, curve_pos[i]);
const float2 screen_co_right = this->project_to_screen(transform, curve_pos_right[i]);
const float2 screen_co_left = this->project_to_screen(transform, curve_pos_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 != curve_pos.size() - 2) {
txt.append(", ");
}
}
{
txt.append(", ");
const float2 screen_co = this->project_to_screen(transform, curve_pos.last());
txt.append(coord_to_svg_string(screen_co));
}
/* Close path (cyclic). */
if (cyclic[curve_i]) {
const float2 screen_co_right = this->project_to_screen(transform, curve_pos_right.last());
const float2 screen_co_left = this->project_to_screen(transform, curve_pos_left.first());
const float2 screen_co = this->project_to_screen(transform, curve_pos.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;
/* Support unicode character paths on Windows. */
#ifdef WIN32
wchar_t *filepath_16 = alloc_utf16_from_8(filepath.c_str(), 0);
std::wstring wstr(filepath_16);
result = main_doc_.save_file(wstr.c_str());
free(filepath_16);
#else
result = main_doc_.save_file(filepath.c_str());
#endif
return result;
}
ExportStatus export_svg(const IOContext &context,
const ExportParams &params,
Scene &scene,
StringRefNull filepath)
{
SVGExporter exporter(context, params);
return exporter.export_scene(scene, filepath);
}
} // namespace blender::io::grease_pencil

View File

@@ -0,0 +1,502 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.hh"
#include "BKE_main.hh"
#include "BLI_assert.h"
#include "BLI_bounds.hh"
#include "BLI_color_types.hh"
#include "BLI_math_color.h"
#include "BLI_math_euler_types.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_report.hh"
#include "DNA_grease_pencil_types.h"
#include "DNA_space_types.h"
#include "DNA_windowmanager_types.h"
#include "ED_grease_pencil.hh"
#include "grease_pencil_io_intern.hh"
#include "nanosvg.h"
#include <fmt/core.h>
#include <fmt/format.h>
namespace blender {
/** \file
* \ingroup bgrease_pencil
*/
using bke::greasepencil::Drawing;
using bke::greasepencil::Layer;
using bke::greasepencil::TreeNode;
namespace io::grease_pencil {
class SVGImporter : public GreasePencilImporter {
public:
using GreasePencilImporter::GreasePencilImporter;
bool read(StringRefNull filepath);
};
static std::string get_layer_id(const NSVGshape &shape, const int prefix)
{
return (shape.id_parent[0] == '\0') ? fmt::format("Layer_{:03d}", prefix) :
fmt::format("{:s}", shape.id_parent);
}
/* Unpack internal NanoSVG color. */
static ColorGeometry4f unpack_nano_color(const uint pack)
{
const uchar4 rgb_u = {uint8_t(((pack) >> 0) & 0xFF),
uint8_t(((pack) >> 8) & 0xFF),
uint8_t(((pack) >> 16) & 0xFF),
uint8_t(((pack) >> 24) & 0xFF)};
const float4 rgb_f = {float(rgb_u[0]) / 255.0f,
float(rgb_u[1]) / 255.0f,
float(rgb_u[2]) / 255.0f,
float(rgb_u[3]) / 255.0f};
ColorGeometry4f color;
srgb_to_linearrgb_v4(color, rgb_f);
return color;
}
/* Simple approximation of a gradient by a single color. */
static ColorGeometry4f average_gradient_color(const NSVGgradient &svg_gradient)
{
const Span<NSVGgradientStop> stops = {svg_gradient.stops, svg_gradient.nstops};
float4 avg_color = float4(0, 0, 0, 0);
if (stops.is_empty()) {
return ColorGeometry4f(avg_color);
}
for (const int i : stops.index_range()) {
avg_color += float4(unpack_nano_color(stops[i].color));
}
avg_color /= stops.size();
return ColorGeometry4f(avg_color);
}
/* TODO Gradients are not yet supported (will output magenta placeholder color).
* This is because gradients for fill materials in particular can only be defined by materials.
* Since each path can have a unique gradient it potentially requires a material per curve.
* Stroke gradients could be baked into vertex colors. */
static ColorGeometry4f convert_svg_color(const NSVGpaint &svg_paint)
{
switch (NSVGpaintType(svg_paint.type)) {
case NSVG_PAINT_UNDEF:
return ColorGeometry4f(1, 0, 1, 1);
case NSVG_PAINT_NONE:
return ColorGeometry4f(0, 0, 0, 1);
case NSVG_PAINT_COLOR:
return unpack_nano_color(svg_paint.color);
case NSVG_PAINT_LINEAR_GRADIENT:
return average_gradient_color(*svg_paint.gradient);
case NSVG_PAINT_RADIAL_GRADIENT:
return average_gradient_color(*svg_paint.gradient);
default:
BLI_assert_unreachable();
return ColorGeometry4f(0, 0, 0, 0);
}
}
/* Make room for curves and points from the SVG shape.
* Returns the index range of newly added curves. */
static IndexRange extend_curves_geometry(bke::CurvesGeometry &curves, const NSVGshape &shape)
{
const int old_curves_num = curves.curves_num();
const int old_points_num = curves.points_num();
const Span<int> old_offsets = curves.offsets();
/* Count curves and points. */
Vector<int> new_curve_offsets;
for (NSVGpath *path = shape.paths; path; path = path->next) {
if (path->npts == 0) {
continue;
}
BLI_assert(path->npts >= 1 && path->npts == int(path->npts / 3) * 3 + 1);
/* nanosvg converts everything to bezier curves, points come in triplets. Round up to the
* next full integer, since there is one point without handles (3*n+1 points in total). */
int point_num = (path->npts + 2) / 3;
/* 2D vectors in triplets: [control point, left handle, right handle]. */
const Span<float2> svg_path_data = Span<float>(path->pts, 2 * path->npts).cast<float2>();
/* Extra points can be at the end of the path, so loop through until they are gone. */
const float2 pos_first = svg_path_data.first();
float2 pos_last = svg_path_data[(point_num - 1) * 3];
while (math::almost_equal_relative(pos_first, pos_last, 1e-6f) && point_num > 1) {
point_num--;
pos_last = svg_path_data[(point_num - 1) * 3];
}
new_curve_offsets.append(point_num);
}
if (new_curve_offsets.is_empty()) {
return {};
}
new_curve_offsets.append(0);
const OffsetIndices new_points_by_curve = offset_indices::accumulate_counts_to_offsets(
new_curve_offsets, old_points_num);
const IndexRange new_curves_range = {old_curves_num, new_points_by_curve.size()};
const int curves_num = new_curves_range.one_after_last();
const int points_num = new_points_by_curve.total_size() + old_points_num;
Array<int> new_offsets(curves_num + 1);
if (old_curves_num > 0) {
new_offsets.as_mutable_span().slice(0, old_curves_num).copy_from(old_offsets.drop_back(1));
}
new_offsets.as_mutable_span()
.slice(old_curves_num, new_curve_offsets.size())
.copy_from(new_curve_offsets);
curves.resize(points_num, curves_num);
curves.offsets_for_write().copy_from(new_offsets);
curves.tag_topology_changed();
return new_curves_range;
}
static void shape_attributes_to_curves(bke::CurvesGeometry &curves,
const NSVGshape &shape,
const IndexRange curves_range,
const float4x4 &transform,
const int shape_index,
const int material_index)
{
/* Path width is twice the radius. */
const float path_width_scale = 0.5f * math::average(math::to_scale(transform));
const OffsetIndices points_by_curve = curves.points_by_curve();
/* nanosvg converts everything to Bezier curves. */
curves.curve_types_for_write().slice(curves_range).fill(CURVE_TYPE_BEZIER);
curves.update_curve_types();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::SpanAttributeWriter<int> materials = attributes.lookup_or_add_for_write_span<int>(
"material_index", bke::AttrDomain::Curve);
MutableSpan<bool> cyclic = curves.cyclic_for_write();
bke::SpanAttributeWriter fill_colors = attributes.lookup_or_add_for_write_span<ColorGeometry4f>(
"fill_color", bke::AttrDomain::Curve);
bke::SpanAttributeWriter<float> fill_opacities = attributes.lookup_or_add_for_write_span<float>(
"fill_opacity", bke::AttrDomain::Curve);
MutableSpan<float3> positions = curves.positions_for_write();
MutableSpan<float3> handle_positions_left = curves.handle_positions_left_for_write();
MutableSpan<float3> handle_positions_right = curves.handle_positions_right_for_write();
MutableSpan<int8_t> handle_types_left = curves.handle_types_left_for_write();
MutableSpan<int8_t> handle_types_right = curves.handle_types_right_for_write();
bke::SpanAttributeWriter<float> radii = attributes.lookup_or_add_for_write_span<float>(
"radius", bke::AttrDomain::Point);
bke::SpanAttributeWriter<ColorGeometry4f> vertex_colors =
attributes.lookup_or_add_for_write_span<ColorGeometry4f>("vertex_color",
bke::AttrDomain::Point);
bke::SpanAttributeWriter<float> point_opacities = attributes.lookup_or_add_for_write_span<float>(
"opacity", bke::AttrDomain::Point);
materials.span.slice(curves_range).fill(material_index);
const ColorGeometry4f shape_color = convert_svg_color(shape.fill);
if (fill_colors) {
fill_colors.span.slice(curves_range).fill(shape_color);
}
if (fill_opacities) {
fill_opacities.span.slice(curves_range).fill(shape_color.a);
}
const bool use_stroke = bool(shape.stroke.type);
const bool use_fill = bool(shape.fill.type);
/* Ensure stroke/fill attributes exist if non-zero values need to be written. */
if (!use_stroke) {
attributes.add<bool>("hide_stroke", bke::AttrDomain::Curve, bke::AttributeInitDefaultValue());
}
if (use_fill) {
attributes.add<int>("fill_id", bke::AttrDomain::Curve, bke::AttributeInitDefaultValue());
}
bke::SpanAttributeWriter<bool> hide_stroke = attributes.lookup_for_write_span<bool>(
"hide_stroke");
bke::SpanAttributeWriter<int> fill_ids = attributes.lookup_for_write_span<int>("fill_id");
if (hide_stroke) {
hide_stroke.span.slice(curves_range).fill(!use_stroke);
hide_stroke.finish();
}
if (fill_ids) {
fill_ids.span.slice(curves_range).fill(use_fill ? shape_index + 1 : 0);
fill_ids.finish();
}
int curve_index = curves_range.start();
for (NSVGpath *path = shape.paths; path; path = path->next) {
if (path->npts == 0) {
continue;
}
const IndexRange points = points_by_curve[curve_index];
/* Close the curve if any points have been removed. An unmodified non-closed curve will have 3
* positions for every point (Center, Left, Right) except for the 2 ends which each remove 1
* (either Left or Right for the Start and End) */
const bool closed = bool(path->closed) || (points.size() * 3 - 2 != path->npts);
cyclic[curve_index] = closed;
/* 2D vectors in triplets: [control point, left handle, right handle]. */
const Span<float2> svg_path_data = Span<float>(path->pts, 2 * path->npts).cast<float2>();
const ColorGeometry4f point_color = convert_svg_color(shape.stroke);
/* Handle first point separately. */
{
const float2 pos_center = svg_path_data.first();
const float2 pos_handle_left = closed ? svg_path_data[points.size() * 3 - 1] : pos_center;
const float2 pos_handle_right = svg_path_data[1];
positions[points.first()] = math::transform_point(transform, float3(pos_center, 0.0f));
handle_positions_left[points.first()] = math::transform_point(transform,
float3(pos_handle_left, 0.0f));
handle_positions_right[points.first()] = math::transform_point(
transform, float3(pos_handle_right, 0.0f));
handle_types_left[points.first()] = BEZIER_HANDLE_FREE;
handle_types_right[points.first()] = BEZIER_HANDLE_FREE;
radii.span[points.first()] = shape.strokeWidth * path_width_scale;
if (vertex_colors) {
vertex_colors.span[points.first()] = point_color;
}
if (point_opacities) {
point_opacities.span[points.first()] = point_color.a;
}
}
for (const int i : points.index_range().drop_front(1)) {
const int point_index = points[i];
const float2 pos_center = svg_path_data[i * 3];
float2 pos_handle_left = svg_path_data[i * 3 - 1];
const float2 pos_handle_right = (i < points.size() - 1 + closed) ? svg_path_data[i * 3 + 1] :
pos_center;
positions[point_index] = math::transform_point(transform, float3(pos_center, 0.0f));
handle_positions_left[point_index] = math::transform_point(transform,
float3(pos_handle_left, 0.0f));
handle_positions_right[point_index] = math::transform_point(transform,
float3(pos_handle_right, 0.0f));
handle_types_left[point_index] = BEZIER_HANDLE_FREE;
handle_types_right[point_index] = BEZIER_HANDLE_FREE;
radii.span[point_index] = shape.strokeWidth * path_width_scale;
if (vertex_colors) {
vertex_colors.span[point_index] = point_color;
}
if (point_opacities) {
point_opacities.span[point_index] = point_color.a;
}
}
++curve_index;
}
materials.finish();
fill_colors.finish();
fill_opacities.finish();
radii.finish();
vertex_colors.finish();
point_opacities.finish();
curves.tag_positions_changed();
curves.tag_radii_changed();
}
static void shift_to_bounds_center(GreasePencil &grease_pencil)
{
const std::optional<Bounds<float3>> bounds = [&]() {
std::optional<Bounds<float3>> bounds;
for (GreasePencilDrawingBase *drawing_base : grease_pencil.drawings()) {
if (drawing_base->type != GP_DRAWING) {
continue;
}
Drawing &drawing = reinterpret_cast<GreasePencilDrawing *>(drawing_base)->wrap();
bounds = bounds::merge(bounds, drawing.strokes().bounds_min_max());
}
return bounds;
}();
if (!bounds) {
return;
}
const float3 offset = -bounds->center();
for (GreasePencilDrawingBase *drawing_base : grease_pencil.drawings()) {
if (drawing_base->type != GP_DRAWING) {
continue;
}
Drawing &drawing = reinterpret_cast<GreasePencilDrawing *>(drawing_base)->wrap();
drawing.strokes_for_write().translate(offset);
drawing.tag_positions_changed();
}
}
bool SVGImporter::read(StringRefNull filepath)
{
/* Fixed SVG unit for scaling. */
constexpr const char *svg_units = "mm";
constexpr float svg_dpi = 96.0f;
char abs_filepath[FILE_MAX];
STRNCPY(abs_filepath, filepath.c_str());
BLI_path_abs(abs_filepath, BKE_main_blendfile_path_from_global());
NSVGimage *svg_data = nullptr;
svg_data = nsvgParseFromFile(abs_filepath, svg_units, svg_dpi);
if (svg_data == nullptr) {
BKE_report(context_.reports, RPT_ERROR, "Could not open SVG");
return false;
}
/* Create grease pencil object. */
char filename[FILE_MAX];
BLI_path_split_file_part(abs_filepath, filename, ARRAY_SIZE(filename));
object_ = create_object(filename);
if (object_ == nullptr) {
BKE_report(context_.reports, RPT_ERROR, "Unable to create new object");
nsvgDelete(svg_data);
return false;
}
GreasePencil &grease_pencil = *id_cast<GreasePencil *>(object_->data);
/* The three possible materials that might be created. */
std::optional<int> mat_index_stroke;
std::optional<int> mat_index_fill;
std::optional<int> mat_index_both;
const float scene_unit_scale = (context_.scene->unit.system != USER_UNIT_NONE &&
params_.use_scene_unit) ?
context_.scene->unit.scale_length :
1.0f;
/* Overall scale for SVG coordinates in millimeters. */
const float svg_scale = 0.001f * scene_unit_scale * params_.scale;
/* Grease pencil is rotated 90 degrees in X axis by default. */
const float4x4 transform = math::scale(
math::from_rotation<float4x4>(math::EulerXYZ(DEG2RAD(-90), 0, 0)), float3(svg_scale));
/* True if any shape has a color gradient, which are not fully supported. */
bool has_color_gradient = false;
/* Loop all shapes. */
std::string prv_id = "*";
int prefix = 0;
int shape_index = 0;
for (NSVGshape *shape = svg_data->shapes; shape; shape = shape->next) {
std::string layer_id = get_layer_id(*shape, prefix);
if (prv_id != layer_id) {
prefix++;
layer_id = get_layer_id(*shape, prefix);
prv_id = layer_id;
}
/* Check if the layer exist and create if needed. */
Layer &layer = [&]() -> Layer & {
TreeNode *layer_node = grease_pencil.find_node_by_name(layer_id);
if (layer_node && layer_node->is_layer()) {
return layer_node->as_layer();
}
Layer &layer = grease_pencil.add_layer(layer_id);
layer.as_node().flag |= GP_LAYER_TREE_NODE_USE_LIGHTS;
return layer;
}();
/* Check frame. */
Drawing *drawing = grease_pencil.get_drawing_at(layer, params_.frame_number);
if (drawing == nullptr) {
drawing = grease_pencil.insert_frame(layer, params_.frame_number);
if (!drawing) {
continue;
}
}
/* Find or create materials. */
const bool is_fill = bool(shape->fill.type);
const bool is_stroke = bool(shape->stroke.type) || !is_fill;
int material_index;
if (is_stroke && is_fill) {
if (!mat_index_both) {
mat_index_both = create_material("Both");
}
material_index = *mat_index_both;
}
else if (is_stroke) {
if (!mat_index_stroke) {
mat_index_stroke = create_material("Stroke");
}
material_index = *mat_index_stroke;
}
else if (is_fill) {
if (!mat_index_fill) {
mat_index_fill = create_material("Fill");
}
material_index = *mat_index_fill;
}
if (ELEM(shape->fill.type, NSVG_PAINT_LINEAR_GRADIENT, NSVG_PAINT_RADIAL_GRADIENT)) {
has_color_gradient = true;
}
bke::CurvesGeometry &curves = drawing->strokes_for_write();
const IndexRange new_curves_range = extend_curves_geometry(curves, *shape);
if (new_curves_range.is_empty()) {
continue;
}
shape_attributes_to_curves(
curves, *shape, new_curves_range, transform, shape_index, material_index);
drawing->strokes_for_write() = std::move(curves);
shape_index++;
}
/* Free SVG memory. */
nsvgDelete(svg_data);
/* Calculate bounding box and move all points to new origin center. */
if (params_.recenter_bounds) {
shift_to_bounds_center(grease_pencil);
}
if (has_color_gradient) {
BKE_report(context_.reports,
RPT_WARNING,
"SVG has gradients, Grease Pencil color will be approximated");
}
return true;
}
bool import_svg(const IOContext &context, const ImportParams &params, StringRefNull filepath)
{
SVGImporter importer(context, params);
return importer.read(filepath);
}
} // namespace io::grease_pencil
} // namespace blender

View File

@@ -0,0 +1,126 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bgrease_pencil
*/
#pragma once
#include "BLI_bounds_types.hh"
#include "BLI_color_types.hh"
#include "BLI_function_ref.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
#include "grease_pencil_io.hh"
#include <cstdint>
#include <optional>
namespace blender {
struct Scene;
struct Object;
struct Material;
struct RegionView3D;
struct GreasePencil;
namespace bke::greasepencil {
class Layer;
class Drawing;
} // namespace bke::greasepencil
namespace io::grease_pencil {
class GreasePencilImporter {
protected:
const IOContext context_;
const ImportParams params_;
Object *object_ = nullptr;
public:
GreasePencilImporter(const IOContext &context, const ImportParams &params);
Object *create_object(StringRefNull name);
int32_t create_material(StringRefNull name);
};
class GreasePencilExporter {
public:
struct ObjectInfo {
Object *object;
float depth;
};
protected:
const IOContext context_;
const ExportParams params_;
/* Camera projection matrix, only available with an active camera. */
std::optional<float4x4> camera_persmat_;
Bounds<float2> camera_rect_;
float2 camera_fac_;
Bounds<float2> screen_rect_;
public:
GreasePencilExporter(const IOContext &context, const ExportParams &params);
void prepare_render_params(Scene &scene, int frame_number);
static ColorGeometry4f compute_average_stroke_color(const Material &material,
const Span<ColorGeometry4f> vertex_colors);
static float compute_average_stroke_opacity(const Span<float> opacities);
/* Returns a value if point sizes are all equal. */
static std::optional<float> try_get_uniform_point_width(const RegionView3D &rv3d,
const Span<float3> world_positions,
const Span<float> radii);
Vector<ObjectInfo> retrieve_objects() const;
using WriteShapeFn = FunctionRef<void(const Span<float3> positions,
const Span<float3> positions_left,
const Span<float3> positions_right,
const OffsetIndices<int> points_by_curve,
const Span<int> shape,
const VArray<bool> &cyclic,
const VArray<int8_t> &types,
const ColorGeometry4f &color,
float opacity,
std::optional<float> width,
std::optional<float> miter_limit_angle,
bool round_cap,
bool is_outline)>;
void foreach_shape_in_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing,
WriteShapeFn shape_fn);
float2 project_to_screen(const float4x4 &transform, const float3 &position) const;
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,
Object &object,
int layer_index,
const bke::greasepencil::Drawing &drawing);
std::optional<Bounds<float2>> compute_objects_bounds(
const RegionView3D &rv3d,
const Depsgraph &depsgraph,
Span<GreasePencilExporter::ObjectInfo> objects,
int frame_number);
};
} // namespace io::grease_pencil
} // namespace blender