Add Chromium-only Blender WebEngine parity work
This commit is contained in:
56
blender-5.2.0/source/blender/io/grease_pencil/CMakeLists.txt
Normal file
56
blender-5.2.0/source/blender/io/grease_pencil/CMakeLists.txt
Normal 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}")
|
||||
@@ -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 ¶ms, StringRefNull filepath);
|
||||
ExportStatus export_svg(const IOContext &context,
|
||||
const ExportParams ¶ms,
|
||||
Scene &scene,
|
||||
StringRefNull filepath);
|
||||
bool export_pdf(const IOContext &context,
|
||||
const ExportParams ¶ms,
|
||||
Scene &scene,
|
||||
StringRefNull filepath);
|
||||
|
||||
} // namespace io::grease_pencil
|
||||
|
||||
} // namespace blender
|
||||
@@ -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 ¶ms)
|
||||
: 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 ¶ms)
|
||||
: 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(¶ms);
|
||||
BKE_camera_params_from_object(¶ms, cam_ob);
|
||||
|
||||
/* Compute matrix, view-plane, etc. */
|
||||
BKE_camera_params_compute_viewplane(
|
||||
¶ms, scene.r.xsch, scene.r.ysch, scene.r.xasp, scene.r.yasp);
|
||||
BKE_camera_params_compute_matrix(¶ms);
|
||||
|
||||
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
|
||||
@@ -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 ¶ms,
|
||||
Scene &scene,
|
||||
StringRefNull filepath)
|
||||
{
|
||||
PDFExporter exporter(context, params);
|
||||
return exporter.export_scene(scene, filepath);
|
||||
}
|
||||
|
||||
} // namespace blender::io::grease_pencil
|
||||
@@ -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 ¶ms,
|
||||
Scene &scene,
|
||||
StringRefNull filepath)
|
||||
{
|
||||
SVGExporter exporter(context, params);
|
||||
return exporter.export_scene(scene, filepath);
|
||||
}
|
||||
|
||||
} // namespace blender::io::grease_pencil
|
||||
@@ -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 ¶ms, StringRefNull filepath)
|
||||
{
|
||||
SVGImporter importer(context, params);
|
||||
return importer.read(filepath);
|
||||
}
|
||||
|
||||
} // namespace io::grease_pencil
|
||||
|
||||
} // namespace blender
|
||||
@@ -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 ¶ms);
|
||||
|
||||
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 ¶ms);
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user