GPv3: Import and export for SVG and PDF

Implements the SVG import/export and PDF export operators for GPv3.

Pull Request: https://projects.blender.org/blender/blender/pulls/123996
This commit is contained in:
Lukas Tönne
2024-08-20 11:41:37 +02:00
committed by Falk David
parent 7a7ae5defe
commit 400c738db9
36 changed files with 2556 additions and 50 deletions

View File

@@ -480,7 +480,7 @@ option(WITH_OPENCOLLADA "Enable OpenCollada Support (http://www.opencollada.org)
option(WITH_IO_WAVEFRONT_OBJ "Enable Wavefront-OBJ 3D file format support (*.obj)" ON)
option(WITH_IO_PLY "Enable PLY 3D file format support (*.ply)" ON)
option(WITH_IO_STL "Enable STL 3D file format support (*.stl)" ON)
option(WITH_IO_GPENCIL "Enable grease-pencil file format IO (*.svg, *.pdf)" ON)
option(WITH_IO_GREASE_PENCIL "Enable grease-pencil file format IO (*.svg, *.pdf)" ON)
# Sound output
option(WITH_SDL "Enable SDL for sound" ON)

View File

@@ -37,7 +37,7 @@ set(WITH_INTERNATIONAL OFF CACHE BOOL "" FORCE)
set(WITH_IO_PLY OFF CACHE BOOL "" FORCE)
set(WITH_IO_STL OFF CACHE BOOL "" FORCE)
set(WITH_IO_WAVEFRONT_OBJ OFF CACHE BOOL "" FORCE)
set(WITH_IO_GPENCIL OFF CACHE BOOL "" FORCE)
set(WITH_IO_GREASE_PENCIL OFF CACHE BOOL "" FORCE)
set(WITH_JACK OFF CACHE BOOL "" FORCE)
set(WITH_LIBMV OFF CACHE BOOL "" FORCE)
set(WITH_LLVM OFF CACHE BOOL "" FORCE)

View File

@@ -893,7 +893,6 @@ class WM_OT_operator_presets_cleanup(Operator):
"WM_OT_collada_import",
"WM_OT_gpencil_export_svg",
"WM_OT_gpencil_export_pdf",
"WM_OT_gpencil_export_svg",
"WM_OT_gpencil_import_svg",
"WM_OT_obj_export",
"WM_OT_obj_import",

View File

@@ -435,7 +435,7 @@ class TOPBAR_MT_file_import(Menu):
"wm.usd_import", text="Universal Scene Description (.usd*)")
if bpy.app.build_options.io_gpencil:
self.layout.operator("wm.gpencil_import_svg", text="SVG as Grease Pencil")
self.layout.operator("wm.grease_pencil_import_svg", text="SVG as Grease Pencil")
if bpy.app.build_options.io_wavefront_obj:
self.layout.operator("wm.obj_import", text="Wavefront (.obj)")
@@ -462,10 +462,10 @@ class TOPBAR_MT_file_export(Menu):
if bpy.app.build_options.io_gpencil:
# PUGIXML library dependency.
if bpy.app.build_options.pugixml:
self.layout.operator("wm.gpencil_export_svg", text="Grease Pencil as SVG")
self.layout.operator("wm.grease_pencil_export_svg", text="Grease Pencil as SVG")
# HARU library dependency.
if bpy.app.build_options.haru:
self.layout.operator("wm.gpencil_export_pdf", text="Grease Pencil as PDF")
self.layout.operator("wm.grease_pencil_export_pdf", text="Grease Pencil as PDF")
if bpy.app.build_options.io_wavefront_obj:
self.layout.operator("wm.obj_export", text="Wavefront (.obj)")

View File

@@ -10,7 +10,8 @@ set(INC
../../io/alembic
../../io/collada
../../io/common
../../io/gpencil
../../io/gpencil_legacy
../../io/grease_pencil
../../io/ply
../../io/stl
../../io/usd
@@ -29,9 +30,10 @@ set(SRC
io_cache.cc
io_collada.cc
io_drop_import_file.cc
io_gpencil_export.cc
io_gpencil_import.cc
io_gpencil_utils.cc
io_gpencil_legacy_export.cc
io_gpencil_legacy_import.cc
io_gpencil_legacy_utils.cc
io_grease_pencil.cc
io_obj.cc
io_ops.cc
io_ply_ops.cc
@@ -43,7 +45,8 @@ set(SRC
io_cache.hh
io_collada.hh
io_drop_import_file.hh
io_gpencil.hh
io_gpencil_legacy.hh
io_grease_pencil.hh
io_obj.hh
io_ops.hh
io_ply_ops.hh
@@ -90,11 +93,12 @@ if(WITH_IO_STL)
add_definitions(-DWITH_IO_STL)
endif()
if(WITH_IO_GPENCIL)
if(WITH_IO_GREASE_PENCIL)
list(APPEND LIB
bf_gpencil
bf_io_gpencil_legacy
bf_io_grease_pencil
)
add_definitions(-DWITH_IO_GPENCIL)
add_definitions(-DWITH_IO_GREASE_PENCIL)
endif()
if(WITH_ALEMBIC)

View File

@@ -24,7 +24,3 @@ void WM_OT_gpencil_export_pdf(wmOperatorType *ot);
ARegion *get_invoke_region(bContext *C);
View3D *get_invoke_view3d(bContext *C);
namespace blender::ed::io {
void gpencil_file_handler_add();
}

View File

@@ -6,7 +6,7 @@
* \ingroup editor/io
*/
#ifdef WITH_IO_GPENCIL
#ifdef WITH_IO_GREASE_PENCIL
# include "BLI_path_util.h"
@@ -28,7 +28,7 @@
# include "WM_api.hh"
# include "WM_types.hh"
# include "io_gpencil.hh"
# include "io_gpencil_legacy.hh"
# include "gpencil_io.h"
@@ -391,4 +391,4 @@ void WM_OT_gpencil_export_pdf(wmOperatorType *ot)
}
# endif /* WITH_HARU */
#endif /* WITH_IO_GPENCIL */
#endif /* WITH_IO_GREASE_PENCIL */

View File

@@ -6,7 +6,7 @@
* \ingroup editor/io
*/
#ifdef WITH_IO_GPENCIL
#ifdef WITH_IO_GREASE_PENCIL
# include "BLI_path_util.h"
# include "BLI_string.h"
@@ -28,7 +28,7 @@
# include "WM_api.hh"
# include "WM_types.hh"
# include "io_gpencil.hh"
# include "io_gpencil_legacy.hh"
# include "io_utils.hh"
# include "gpencil_io.h"
@@ -172,17 +172,4 @@ void WM_OT_gpencil_import_svg(wmOperatorType *ot)
100.0f);
}
namespace blender::ed::io {
void gpencil_file_handler_add()
{
auto fh = std::make_unique<blender::bke::FileHandlerType>();
STRNCPY(fh->idname, "IO_FH_gpencil_svg");
STRNCPY(fh->import_operator, "WM_OT_gpencil_import_svg");
STRNCPY(fh->label, "SVG as Grease Pencil");
STRNCPY(fh->file_extensions_str, ".svg");
fh->poll_drop = poll_file_object_drop;
bke::file_handler_add(std::move(fh));
}
} // namespace blender::ed::io
#endif /* WITH_IO_GPENCIL */
#endif /* WITH_IO_GREASE_PENCIL */

View File

@@ -6,7 +6,7 @@
* \ingroup editor/io
*/
#ifdef WITH_IO_GPENCIL
#ifdef WITH_IO_GREASE_PENCIL
# include "DNA_space_types.h"
@@ -15,7 +15,7 @@
# include "WM_api.hh"
# include "io_gpencil.hh"
# include "io_gpencil_legacy.hh"
ARegion *get_invoke_region(bContext *C)
{
@@ -50,4 +50,4 @@ View3D *get_invoke_view3d(bContext *C)
return nullptr;
}
#endif /* WITH_IO_GPENCIL */
#endif /* WITH_IO_GREASE_PENCIL */

View File

@@ -0,0 +1,597 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#include "DNA_view3d_types.h"
#ifdef WITH_IO_GREASE_PENCIL
# include "BLI_path_util.h"
# include "BLI_string.h"
# include "DNA_space_types.h"
# include "BKE_context.hh"
# include "BKE_file_handler.hh"
# include "BKE_report.hh"
# include "BKE_screen.hh"
# include "BLT_translation.hh"
# include "RNA_access.hh"
# include "RNA_define.hh"
# include "ED_fileselect.hh"
# include "UI_interface.hh"
# include "UI_resources.hh"
# include "WM_api.hh"
# include "WM_types.hh"
# include "io_grease_pencil.hh"
# include "io_utils.hh"
# include "grease_pencil_io.hh"
# if defined(WITH_PUGIXML) || defined(WITH_HARU)
namespace blender::ed::io {
/* Definition of enum elements to export. */
/* Common props for exporting. */
static void grease_pencil_export_common_props_definition(wmOperatorType *ot)
{
using blender::io::grease_pencil::ExportParams;
using SelectMode = ExportParams::SelectMode;
static const EnumPropertyItem select_mode_items[] = {
{int(SelectMode::Active), "ACTIVE", 0, "Active", "Include only the active object"},
{int(SelectMode::Selected), "SELECTED", 0, "Selected", "Include selected objects"},
{int(SelectMode::Visible), "VISIBLE", 0, "Visible", "Include all visible objects"},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_boolean(ot->srna, "use_fill", true, "Fill", "Export strokes with fill enabled");
RNA_def_enum(ot->srna,
"selected_object_type",
select_mode_items,
int(SelectMode::Active),
"Object",
"Which objects to include in the export");
RNA_def_float(ot->srna,
"stroke_sample",
0.0f,
0.0f,
100.0f,
"Sampling",
"Precision of stroke sampling. Low values mean a more precise result, and zero "
"disables sampling",
0.0f,
100.0f);
RNA_def_boolean(
ot->srna, "use_uniform_width", false, "Uniform Width", "Export strokes with uniform width");
}
/* Note: Region data is found using "big area" functions, rather than context. This is necessary
* since export operators are not always invoked from a View3D. This enables the operator to find
* the most relevant 3D view for projection of strokes. */
static bool get_invoke_region(bContext *C,
ARegion **r_region,
View3D **r_view3d,
RegionView3D **r_rv3d)
{
bScreen *screen = CTX_wm_screen(C);
if (screen == nullptr) {
return false;
}
ScrArea *area = BKE_screen_find_big_area(screen, SPACE_VIEW3D, 0);
if (area == nullptr) {
return false;
}
ARegion *region = BKE_area_find_region_type(area, RGN_TYPE_WINDOW);
*r_region = region;
*r_view3d = static_cast<View3D *>(area->spacedata.first);
*r_rv3d = static_cast<RegionView3D *>(region->regiondata);
return true;
}
} // namespace blender::ed::io
# endif
/* -------------------------------------------------------------------- */
/** \name SVG single frame import
* \{ */
namespace blender::ed::io {
static bool grease_pencil_import_svg_check(bContext * /*C*/, wmOperator *op)
{
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".svg")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".svg");
RNA_string_set(op->ptr, "filepath", filepath);
return true;
}
return false;
}
static int grease_pencil_import_svg_exec(bContext *C, wmOperator *op)
{
using blender::io::grease_pencil::ImportParams;
using blender::io::grease_pencil::IOContext;
Scene *scene = CTX_data_scene(C);
if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false) ||
!RNA_struct_find_property(op->ptr, "directory"))
{
BKE_report(op->reports, RPT_ERROR, "No filepath given");
return OPERATOR_CANCELLED;
}
ARegion *region;
View3D *v3d;
RegionView3D *rv3d;
if (!get_invoke_region(C, &region, &v3d, &rv3d)) {
BKE_report(op->reports, RPT_ERROR, "Unable to find valid 3D View area");
return OPERATOR_CANCELLED;
}
const int resolution = RNA_int_get(op->ptr, "resolution");
const float scale = RNA_float_get(op->ptr, "scale");
const bool use_scene_unit = RNA_boolean_get(op->ptr, "use_scene_unit");
const bool recenter_bounds = true;
const IOContext io_context(*C, region, v3d, rv3d, op->reports);
const ImportParams params = {scale, scene->r.cfra, resolution, use_scene_unit, recenter_bounds};
/* Loop all selected files to import them. All SVG imported shared the same import
* parameters, but they are created in separated grease pencil objects. */
const auto paths = blender::ed::io::paths_from_operator_properties(op->ptr);
for (const auto &path : paths) {
/* Do Import. */
WM_cursor_wait(true);
const bool done = blender::io::grease_pencil::import_svg(io_context, params, path);
WM_cursor_wait(false);
if (!done) {
BKE_reportf(op->reports, RPT_WARNING, "Unable to import '%s'", path.c_str());
}
}
return OPERATOR_FINISHED;
}
static void grease_pencil_import_svg_draw(bContext * /*C*/, wmOperator *op)
{
uiLayout *layout = op->layout;
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
uiLayout *box = uiLayoutBox(layout);
uiLayout *col = uiLayoutColumn(box, false);
uiItemR(col, op->ptr, "resolution", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, op->ptr, "scale", UI_ITEM_NONE, nullptr, ICON_NONE);
}
static bool grease_pencil_import_svg_poll(bContext *C)
{
if ((CTX_wm_window(C) == nullptr) || (CTX_data_mode_enum(C) != CTX_MODE_OBJECT)) {
return false;
}
return true;
}
} // namespace blender::ed::io
void WM_OT_grease_pencil_import_svg(wmOperatorType *ot)
{
ot->name = "Import SVG as Grease Pencil";
ot->description = "Import SVG into grease pencil";
ot->idname = "WM_OT_grease_pencil_import_svg";
ot->invoke = blender::ed::io::filesel_drop_import_invoke;
ot->exec = blender::ed::io::grease_pencil_import_svg_exec;
ot->poll = blender::ed::io::grease_pencil_import_svg_poll;
ot->ui = blender::ed::io::grease_pencil_import_svg_draw;
ot->check = blender::ed::io::grease_pencil_import_svg_check;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO,
FILE_BLENDER,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_RELPATH | WM_FILESEL_SHOW_PROPS |
WM_FILESEL_DIRECTORY | WM_FILESEL_FILES,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
RNA_def_int(ot->srna,
"resolution",
10,
1,
100000,
"Resolution",
"Resolution of the generated strokes",
1,
20);
RNA_def_float(ot->srna,
"scale",
10.0f,
0.000001f,
1000000.0f,
"Scale",
"Scale of the final strokes",
0.001f,
100.0f);
RNA_def_boolean(ot->srna,
"use_scene_unit",
false,
"Scene Unit",
"Apply current scene's unit (as defined by unit scale) to imported data");
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name SVG single frame export
* \{ */
# ifdef WITH_PUGIXML
namespace blender::ed::io {
static bool grease_pencil_export_svg_check(bContext * /*C*/, wmOperator *op)
{
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".svg")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".svg");
RNA_string_set(op->ptr, "filepath", filepath);
return true;
}
return false;
}
static int grease_pencil_export_svg_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
ED_fileselect_ensure_default_filepath(C, op, ".svg");
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int grease_pencil_export_svg_exec(bContext *C, wmOperator *op)
{
using blender::io::grease_pencil::ExportParams;
using blender::io::grease_pencil::IOContext;
Scene *scene = CTX_data_scene(C);
Object *ob = CTX_data_active_object(C);
if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
BKE_report(op->reports, RPT_ERROR, "No filepath given");
return OPERATOR_CANCELLED;
}
ARegion *region;
View3D *v3d;
RegionView3D *rv3d;
if (!get_invoke_region(C, &region, &v3d, &rv3d)) {
BKE_report(op->reports, RPT_ERROR, "Unable to find valid 3D View area");
return OPERATOR_CANCELLED;
}
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
const bool export_stroke_materials = true;
const bool export_fill_materials = RNA_boolean_get(op->ptr, "use_fill");
const bool use_uniform_width = RNA_boolean_get(op->ptr, "use_uniform_width");
const ExportParams::SelectMode select_mode = ExportParams::SelectMode(
RNA_enum_get(op->ptr, "selected_object_type"));
const ExportParams::FrameMode frame_mode = ExportParams::FrameMode::Active;
const bool use_clip_camera = RNA_boolean_get(op->ptr, "use_clip_camera");
const float stroke_sample = RNA_float_get(op->ptr, "stroke_sample");
const IOContext io_context(*C, region, v3d, rv3d, op->reports);
const ExportParams params = {ob,
select_mode,
frame_mode,
export_stroke_materials,
export_fill_materials,
use_clip_camera,
use_uniform_width,
stroke_sample};
WM_cursor_wait(true);
const bool done = blender::io::grease_pencil::export_svg(io_context, params, *scene, filepath);
WM_cursor_wait(false);
if (!done) {
BKE_report(op->reports, RPT_WARNING, "Unable to export SVG");
}
return OPERATOR_FINISHED;
}
static void grease_pencil_export_svg_draw(bContext * /*C*/, wmOperator *op)
{
uiLayout *layout = op->layout;
uiLayout *box, *row;
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
box = uiLayoutBox(layout);
row = uiLayoutRow(box, false);
uiItemL(row, IFACE_("Scene Options"), ICON_NONE);
row = uiLayoutRow(box, false);
uiItemR(row, op->ptr, "selected_object_type", UI_ITEM_NONE, nullptr, ICON_NONE);
box = uiLayoutBox(layout);
row = uiLayoutRow(box, false);
uiItemL(row, IFACE_("Export Options"), ICON_NONE);
uiLayout *col = uiLayoutColumn(box, false);
uiItemR(col, op->ptr, "stroke_sample", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, op->ptr, "use_fill", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, op->ptr, "use_uniform_width", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, op->ptr, "use_clip_camera", UI_ITEM_NONE, nullptr, ICON_NONE);
}
static bool grease_pencil_export_svg_poll(bContext *C)
{
if ((CTX_wm_window(C) == nullptr) || (CTX_data_mode_enum(C) != CTX_MODE_OBJECT)) {
return false;
}
return true;
}
} // namespace blender::ed::io
void WM_OT_grease_pencil_export_svg(wmOperatorType *ot)
{
ot->name = "Export to SVG";
ot->description = "Export grease pencil to SVG";
ot->idname = "WM_OT_grease_pencil_export_svg";
ot->invoke = blender::ed::io::grease_pencil_export_svg_invoke;
ot->exec = blender::ed::io::grease_pencil_export_svg_exec;
ot->poll = blender::ed::io::grease_pencil_export_svg_poll;
ot->ui = blender::ed::io::grease_pencil_export_svg_draw;
ot->check = blender::ed::io::grease_pencil_export_svg_check;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
blender::ed::io::grease_pencil_export_common_props_definition(ot);
RNA_def_boolean(ot->srna,
"use_clip_camera",
false,
"Clip Camera",
"Clip drawings to camera size when exporting in camera view");
}
# endif
/** \} */
/* -------------------------------------------------------------------- */
/** \name PDF single frame export
* \{ */
# ifdef WITH_HARU
namespace blender::ed::io {
static bool grease_pencil_export_pdf_check(bContext * /*C*/, wmOperator *op)
{
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".pdf")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".pdf");
RNA_string_set(op->ptr, "filepath", filepath);
return true;
}
return false;
}
static int grease_pencil_export_pdf_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/)
{
ED_fileselect_ensure_default_filepath(C, op, ".pdf");
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int grease_pencil_export_pdf_exec(bContext *C, wmOperator *op)
{
using blender::io::grease_pencil::ExportParams;
using blender::io::grease_pencil::IOContext;
Scene *scene = CTX_data_scene(C);
Object *ob = CTX_data_active_object(C);
if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
BKE_report(op->reports, RPT_ERROR, "No filepath given");
return OPERATOR_CANCELLED;
}
ARegion *region;
View3D *v3d;
RegionView3D *rv3d;
if (!get_invoke_region(C, &region, &v3d, &rv3d)) {
BKE_report(op->reports, RPT_ERROR, "Unable to find valid 3D View area");
return OPERATOR_CANCELLED;
}
char filepath[FILE_MAX];
RNA_string_get(op->ptr, "filepath", filepath);
const bool export_stroke_materials = true;
const bool export_fill_materials = RNA_boolean_get(op->ptr, "use_fill");
const bool use_uniform_width = RNA_boolean_get(op->ptr, "use_uniform_width");
const ExportParams::SelectMode select_mode = ExportParams::SelectMode(
RNA_enum_get(op->ptr, "selected_object_type"));
const ExportParams::FrameMode frame_mode = ExportParams::FrameMode(
RNA_enum_get(op->ptr, "frame_mode"));
const bool use_clip_camera = false;
const float stroke_sample = RNA_float_get(op->ptr, "stroke_sample");
const IOContext io_context(*C, region, v3d, rv3d, op->reports);
const ExportParams params = {ob,
select_mode,
frame_mode,
export_stroke_materials,
export_fill_materials,
use_clip_camera,
use_uniform_width,
stroke_sample};
WM_cursor_wait(true);
const bool done = blender::io::grease_pencil::export_pdf(io_context, params, *scene, filepath);
WM_cursor_wait(false);
if (!done) {
BKE_report(op->reports, RPT_WARNING, "Unable to export PDF");
}
return OPERATOR_FINISHED;
}
static void ui_gpencil_export_pdf_settings(uiLayout *layout, PointerRNA *imfptr)
{
uiLayout *box, *row, *col, *sub;
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
box = uiLayoutBox(layout);
row = uiLayoutRow(box, false);
uiItemL(row, IFACE_("Scene Options"), ICON_NONE);
row = uiLayoutRow(box, false);
uiItemR(row, imfptr, "selected_object_type", UI_ITEM_NONE, nullptr, ICON_NONE);
box = uiLayoutBox(layout);
row = uiLayoutRow(box, false);
uiItemL(row, IFACE_("Export Options"), ICON_NONE);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumn(col, true);
uiItemR(sub, imfptr, "frame_mode", UI_ITEM_NONE, IFACE_("Frame"), ICON_NONE);
uiLayoutSetPropSep(box, true);
sub = uiLayoutColumn(col, true);
uiItemR(sub, imfptr, "stroke_sample", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(sub, imfptr, "use_fill", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(sub, imfptr, "use_uniform_width", UI_ITEM_NONE, nullptr, ICON_NONE);
}
static void grease_pencil_export_pdf_draw(bContext * /*C*/, wmOperator *op)
{
ui_gpencil_export_pdf_settings(op->layout, op->ptr);
}
static bool grease_pencil_export_pdf_poll(bContext *C)
{
if ((CTX_wm_window(C) == nullptr) || (CTX_data_mode_enum(C) != CTX_MODE_OBJECT)) {
return false;
}
return true;
}
} // namespace blender::ed::io
void WM_OT_grease_pencil_export_pdf(wmOperatorType *ot)
{
ot->name = "Export to PDF";
ot->description = "Export grease pencil to PDF";
ot->idname = "WM_OT_grease_pencil_export_pdf";
ot->invoke = blender::ed::io::grease_pencil_export_pdf_invoke;
ot->exec = blender::ed::io::grease_pencil_export_pdf_exec;
ot->poll = blender::ed::io::grease_pencil_export_pdf_poll;
ot->ui = blender::ed::io::grease_pencil_export_pdf_draw;
ot->check = blender::ed::io::grease_pencil_export_pdf_check;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
using blender::io::grease_pencil::ExportParams;
static const EnumPropertyItem frame_mode_items[] = {
{int(ExportParams::FrameMode::Active), "ACTIVE", 0, "Active", "Include only active frame"},
{int(ExportParams::FrameMode::Selected),
"SELECTED",
0,
"Selected",
"Include selected frames"},
{int(ExportParams::FrameMode::Scene), "SCENE", 0, "Scene", "Include all scene frames"},
{0, nullptr, 0, nullptr, nullptr},
};
blender::ed::io::grease_pencil_export_common_props_definition(ot);
ot->prop = RNA_def_enum(ot->srna,
"frame_mode",
frame_mode_items,
int(ExportParams::FrameMode::Active),
"Frames",
"Which frames to include in the export");
}
# endif /* WITH_HARU */
/** \} */
namespace blender::ed::io {
void grease_pencil_file_handler_add()
{
auto fh = std::make_unique<blender::bke::FileHandlerType>();
STRNCPY(fh->idname, "IO_FH_grease_pencil_svg");
STRNCPY(fh->import_operator, "WM_OT_grease_pencil_import_svg");
STRNCPY(fh->label, "SVG as Grease Pencil");
STRNCPY(fh->file_extensions_str, ".svg");
fh->poll_drop = poll_file_object_drop;
bke::file_handler_add(std::move(fh));
}
} // namespace blender::ed::io
#endif /* WITH_IO_GREASE_PENCIL */

View File

@@ -0,0 +1,27 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#pragma once
struct ARegion;
struct View3D;
struct bContext;
struct wmOperatorType;
void WM_OT_grease_pencil_import_svg(wmOperatorType *ot);
#ifdef WITH_PUGIXML
void WM_OT_grease_pencil_export_svg(wmOperatorType *ot);
#endif
#ifdef WITH_HARU
void WM_OT_grease_pencil_export_pdf(wmOperatorType *ot);
#endif
namespace blender::ed::io {
void grease_pencil_file_handler_add();
}

View File

@@ -24,7 +24,8 @@
#include "io_cache.hh"
#include "io_drop_import_file.hh"
#include "io_gpencil.hh"
#include "io_gpencil_legacy.hh"
#include "io_grease_pencil.hh"
#include "io_obj.hh"
#include "io_ply_ops.hh"
#include "io_stl_ops.hh"
@@ -49,14 +50,17 @@ void ED_operatortypes_io()
ed::io::usd_file_handler_add();
#endif
#ifdef WITH_IO_GPENCIL
#ifdef WITH_IO_GREASE_PENCIL
WM_operatortype_append(WM_OT_gpencil_import_svg);
ed::io::gpencil_file_handler_add();
WM_operatortype_append(WM_OT_grease_pencil_import_svg);
ed::io::grease_pencil_file_handler_add();
# ifdef WITH_PUGIXML
WM_operatortype_append(WM_OT_gpencil_export_svg);
WM_operatortype_append(WM_OT_grease_pencil_export_svg);
# endif
# ifdef WITH_HARU
WM_operatortype_append(WM_OT_gpencil_export_pdf);
WM_operatortype_append(WM_OT_grease_pencil_export_pdf);
# endif
#endif

View File

@@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-2.0-or-later
if(WITH_IO_WAVEFRONT_OBJ OR WITH_IO_PLY OR WITH_IO_STL OR WITH_IO_GPENCIL OR WITH_ALEMBIC OR WITH_USD)
if(WITH_IO_WAVEFRONT_OBJ OR WITH_IO_PLY OR WITH_IO_STL OR WITH_IO_GREASE_PENCIL OR WITH_ALEMBIC OR WITH_USD)
add_subdirectory(common)
endif()
@@ -18,8 +18,9 @@ if(WITH_IO_STL)
add_subdirectory(stl)
endif()
if(WITH_IO_GPENCIL)
add_subdirectory(gpencil)
if(WITH_IO_GREASE_PENCIL)
add_subdirectory(gpencil_legacy)
add_subdirectory(grease_pencil)
endif()
if(WITH_ALEMBIC)

View File

@@ -84,4 +84,4 @@ if(WITH_BOOST)
)
endif()
blender_add_lib(bf_gpencil "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
blender_add_lib(bf_io_gpencil_legacy "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: 2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
../common
../../blenkernel
../../blenloader
../../bmesh
../../editors/include
../../functions
../../geometry
../../makesdna
../../makesrna
../../windowmanager
../../../../intern/guardedalloc
../../../../intern/utfconv
../../../../extern/fmtlib/include
)
set(INC_SYS
)
set(SRC
intern/grease_pencil_io_import_svg.cc
intern/grease_pencil_io.cc
grease_pencil_io.hh
intern/grease_pencil_io_intern.hh
)
set(LIB
bf_blenkernel
PRIVATE bf::blenlib
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::extern::nanosvg
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE extern_fmtlib
bf_io_common
)
if(WITH_PUGIXML)
list(APPEND SRC
intern/grease_pencil_io_export_svg.cc
)
list(APPEND INC_SYS
${PUGIXML_INCLUDE_DIR}
)
list(APPEND LIB
${PUGIXML_LIBRARIES}
)
add_definitions(-DWITH_PUGIXML)
endif()
if(WITH_HARU)
list(APPEND SRC
intern/grease_pencil_io_export_pdf.cc
)
list(APPEND INC_SYS
${HARU_INCLUDE_DIRS}
)
list(APPEND LIB
${HARU_LIBRARIES}
# Haru needs `TIFFFaxBlackCodes` & `TIFFFaxWhiteCodes` symbols from TIFF.
# Can be removed with Haru 2.4.0. They should be shipping with their own
# Fax codes defined by default from that version onwards.
${TIFF_LIBRARY}
)
add_definitions(-DWITH_HARU)
endif()
if(WITH_BOOST)
list(APPEND LIB
${BOOST_LIBRARIES}
)
endif()
blender_add_lib(bf_io_grease_pencil "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")

View File

@@ -0,0 +1,86 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_string_ref.hh"
#include "DNA_view3d_types.h"
#pragma once
/** \file
* \ingroup bgrease_pencil
*/
struct ARegion;
struct View3D;
struct bContext;
struct Scene;
struct ReportList;
struct Depsgraph;
namespace blender::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;
};
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);
bool 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 blender::io::grease_pencil

View File

@@ -0,0 +1,578 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "BLI_color.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.h"
#include "BKE_attribute.hh"
#include "BKE_camera.h"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_gpencil_legacy.h"
#include "BKE_grease_pencil.hh"
#include "BKE_layer.hh"
#include "BKE_material.h"
#include "BKE_scene.hh"
#include "BLI_math_vector.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 "grease_pencil_io_intern.hh"
#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(0);
}
/* 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);
}
static std::optional<float> try_get_constant_value(const VArray<float> values,
const float epsilon = 1e-5f)
{
if (values.is_empty()) {
return std::nullopt;
}
const float first_value = values.first();
const std::optional<float> first_value_opt = std::make_optional(first_value);
return threading::parallel_reduce(
values.index_range().drop_front(1),
4096,
first_value_opt,
[&](const IndexRange range, const std::optional<float> /*value*/) -> std::optional<float> {
for (const int i : range) {
if (math::abs(values[i] - first_value) > epsilon) {
return std::nullopt;
}
}
return first_value_opt;
},
[&](const std::optional<float> a, const std::optional<float> b) {
return (a && b) ? first_value_opt : std::nullopt;
});
}
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 = blender::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 bool stroke,
const bool fill)
{
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_gpencil_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_gpencil_object_material_new(bmain, object_, name.c_str(), &new_idx);
MaterialGPencilStyle *gp_style = mat_gp->gp_style;
gp_style->flag &= ~GP_MATERIAL_STROKE_SHOW;
gp_style->flag &= ~GP_MATERIAL_FILL_SHOW;
copy_v4_v4(gp_style->stroke_rgba, default_stroke_color);
copy_v4_v4(gp_style->fill_rgba, default_fill_color);
if (stroke) {
gp_style->flag |= GP_MATERIAL_STROKE_SHOW;
}
if (fill) {
gp_style->flag |= GP_MATERIAL_FILL_SHOW;
}
mat_index = object_->totcol - 1;
}
return mat_index;
}
GreasePencilExporter::GreasePencilExporter(const IOContext &context, const ExportParams &params)
: context_(context), params_(params)
{
}
constexpr const char *attr_material_index = "material_index";
static IndexMask get_visible_strokes(const Object &object,
const bke::greasepencil::Drawing &drawing,
IndexMaskMemory &memory)
{
const bke::CurvesGeometry &strokes = drawing.strokes();
const bke::AttributeAccessor attributes = strokes.attributes();
const VArray<int> materials = *attributes.lookup<int>(attr_material_index,
bke::AttrDomain::Curve);
auto is_visible_curve = [&](const int curve_i) {
/* Check if stroke can be drawn. */
const IndexRange points = strokes.points_by_curve()[curve_i];
if (points.size() < 2) {
return false;
}
/* Check if the material is visible. */
const Material *material = BKE_object_material_get(const_cast<Object *>(&object),
materials[curve_i] + 1);
const MaterialGPencilStyle *gp_style = material ? material->gp_style : nullptr;
const bool is_hidden_material = (gp_style->flag & GP_MATERIAL_HIDE);
const bool is_stroke_material = (gp_style->flag & GP_MATERIAL_STROKE_SHOW);
if (gp_style == nullptr || is_hidden_material || !is_stroke_material) {
return false;
}
return true;
};
return IndexMask::from_predicate(
strokes.curves_range(), GrainSize(512), memory, is_visible_curve);
}
static std::optional<Bounds<float2>> compute_drawing_bounds(
const ARegion &region,
const RegionView3D &rv3d,
const Object &object,
const Object &object_eval,
const int layer_index,
const int frame_number,
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 = *static_cast<GreasePencil *>(object.data);
if (!grease_pencil.has_active_layer()) {
return drawing_bounds;
}
const Layer &layer = *grease_pencil.layers()[layer_index];
const float4x4 layer_to_world = layer.to_world_space(object);
const bke::crazyspace::GeometryDeformation deformation =
bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation(
&object_eval, object, layer_index, frame_number);
const VArray<float> radii = drawing.radii();
const bke::CurvesGeometry &strokes = drawing.strokes();
IndexMaskMemory curve_mask_memory;
const IndexMask curve_mask = get_visible_strokes(object, drawing, curve_mask_memory);
curve_mask.foreach_index(GrainSize(512), [&](const int curve_i) {
const IndexRange points = strokes.points_by_curve()[curve_i];
/* Check if stroke can be drawn. */
if (points.size() < 2) {
return;
}
for (const int point_i : points) {
const float3 pos_world = math::transform_point(layer_to_world,
deformation.positions[point_i]);
float2 pos_view;
eV3DProjStatus result = ED_view3d_project_float_global(
&region, pos_world, pos_view, V3D_PROJ_TEST_NOP);
if (result == V3D_PROJ_RET_OK) {
const float pixels = radii[point_i] / ED_view3d_pixel_size(&rv3d, pos_world);
std::optional<Bounds<float2>> point_bounds = Bounds<float2>(pos_view);
point_bounds->pad(pixels);
drawing_bounds = bounds::merge(drawing_bounds, point_bounds);
}
}
});
return drawing_bounds;
}
static std::optional<Bounds<float2>> compute_objects_bounds(
const ARegion &region,
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) {
const Object *object_eval = DEG_get_evaluated_object(&depsgraph, info.object);
const GreasePencil &grease_pencil_eval = *static_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 = compute_drawing_bounds(
region, rv3d, *info.object, *object_eval, layer_index, frame_number, *drawing);
full_bounds = bounds::merge(full_bounds, layer_bounds);
}
}
/* Add small gap. */
full_bounds->pad(gap);
return full_bounds;
}
void GreasePencilExporter::prepare_camera_params(Scene &scene,
const int frame_number,
const bool force_camera_view)
{
const bool use_camera_view = force_camera_view && (context_.v3d->camera != nullptr);
/* Ensure camera switch is applied. */
BKE_scene_camera_switch_update(&scene);
/* Calculate camera matrix. */
Object *cam_ob = scene.camera;
if (cam_ob != nullptr) {
/* 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());
persmat_ = float4x4(params.winmat) * viewmat;
}
else {
persmat_ = float4x4::identity();
}
win_size_ = {context_.region->winx, context_.region->winy};
/* Camera rectangle. */
if ((context_.rv3d->persp == RV3D_CAMOB) || (use_camera_view)) {
BKE_render_resolution(&scene.r, false, &render_size_.x, &render_size_.y);
ED_view3d_calc_camera_border(&scene,
context_.depsgraph,
context_.region,
context_.v3d,
context_.rv3d,
&camera_rect_,
true);
is_camera_ = true;
camera_ratio_ = render_size_.x / (camera_rect_.xmax - camera_rect_.xmin);
offset_.x = camera_rect_.xmin;
offset_.y = camera_rect_.ymin;
}
else {
is_camera_ = false;
Vector<ObjectInfo> objects = this->retrieve_objects();
std::optional<Bounds<float2>> full_bounds = compute_objects_bounds(
*context_.region, *context_.rv3d, *context_.depsgraph, objects, frame_number);
if (full_bounds) {
render_size_ = int2(full_bounds->size());
offset_ = full_bounds->min;
}
else {
camera_ratio_ = 1.0f;
offset_ = float2(0);
}
}
}
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)
{
VArray<float> widths = VArray<float>::ForFunc(world_positions.size(), [&](const int index) {
const float3 &pos = world_positions[index];
const float radius = radii[index];
return 2.0f * radius * ED_view3d_pixel_size(&rv3d, pos);
});
return try_get_constant_value(widths);
}
Vector<GreasePencilExporter::ObjectInfo> GreasePencilExporter::retrieve_objects() const
{
using SelectMode = ExportParams::SelectMode;
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(&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 = is_camera_ || !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:
LISTBASE_FOREACH (Base *, base, BKE_view_layer_object_bases_get(view_layer)) {
if (base->flag & BASE_SELECTED) {
add_object(base->object);
}
}
break;
case SelectMode::Visible:
LISTBASE_FOREACH (Base *, base, BKE_view_layer_object_bases_get(view_layer)) {
add_object(base->object);
}
break;
}
/* Sort list of objects from point of view. */
std::sort(objects.begin(), objects.end(), [](const ObjectInfo &info1, const ObjectInfo &info2) {
return info1.depth < info2.depth;
});
return objects;
}
void GreasePencilExporter::foreach_stroke_in_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing,
WriteStrokeFn stroke_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);
/* Point attributes. */
const Span<float3> positions = curves.positions();
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());
threading::parallel_for(positions.index_range(), 4096, [&](const IndexRange range) {
for (const int i : range) {
world_positions[i] = math::transform_point(layer_to_world, positions[i]);
}
});
for (const int i_curve : curves.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
if (points.size() < 2) {
continue;
}
const bool is_cyclic = cyclic[i_curve];
const int material_index = material_indices[i_curve];
const Material *material = BKE_object_material_get(const_cast<Object *>(&object),
material_index + 1);
BLI_assert(material->gp_style != nullptr);
if (material->gp_style->flag & GP_MATERIAL_HIDE) {
continue;
}
const bool is_stroke_material = material->gp_style->flag & GP_MATERIAL_STROKE_SHOW;
const bool is_fill_material = material->gp_style->flag & GP_MATERIAL_FILL_SHOW;
/* Fill. */
if (is_fill_material && params_.export_fill_materials) {
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);
stroke_fn(positions.slice(points),
is_cyclic,
fill_color,
layer.opacity,
std::nullopt,
false,
false);
}
/* Stroke. */
if (is_stroke_material && params_.export_stroke_materials) {
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 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;
stroke_fn(positions.slice(points),
is_cyclic,
stroke_color,
stroke_opacity,
uniform_width,
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>::ForSingle(
params_.outline_resample_length, curves.curves_num());
outline = geometry::resample_to_length(outline, single_curve_mask, resample_lengths);
}
const OffsetIndices outline_points_by_curve = outline.points_by_curve();
const Span<float3> outline_positions = outline.positions();
for (const int i_outline_curve : outline.curves_range()) {
const IndexRange outline_points = outline_points_by_curve[i_outline_curve];
/* Use stroke color to fill the outline. */
stroke_fn(outline_positions.slice(outline_points),
true,
stroke_color,
stroke_opacity,
std::nullopt,
false,
true);
}
}
}
}
}
float2 GreasePencilExporter::project_to_screen(const float4x4 &transform,
const float3 &position) const
{
float2 screen_co = float2(0.0f);
if (ED_view3d_project_float_ex(context_.region,
const_cast<float(*)[4]>(context_.rv3d->winmat),
false,
math::transform_point(transform, position),
screen_co,
V3D_PROJ_TEST_NOP) == V3D_PROJ_RET_OK)
{
return screen_co;
}
return float2(0.0f);
}
} // namespace blender::io::grease_pencil

View File

@@ -0,0 +1,288 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_material.h"
#include "BKE_scene.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_grease_pencil_types.h"
#include "DNA_material_types.h"
#include "DNA_scene_types.h"
#include "ED_view3d.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();
void write_stroke_to_polyline(const float4x4 &transform,
const Span<float3> positions,
const bool cyclic,
const ColorGeometry4f &color,
const float opacity,
std::optional<float> width);
bool write_to_file(StringRefNull filepath);
};
static bool is_selected_frame(const GreasePencil &grease_pencil, const int frame_number)
{
for (const bke::greasepencil::Layer *layer : grease_pencil.layers()) {
if (layer->is_visible()) {
const GreasePencilFrame *frame = layer->frame_at(frame_number);
if (frame->is_selected()) {
return true;
}
}
}
return false;
}
bool PDFExporter::export_scene(Scene &scene, StringRefNull filepath)
{
bool result = false;
Object &ob_eval = *DEG_get_evaluated_object(context_.depsgraph, params_.object);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(ob_eval.data);
if (!create_document()) {
return false;
}
switch (params_.frame_mode) {
case ExportParams::FrameMode::Active: {
const int frame_number = scene.r.cfra;
this->prepare_camera_params(scene, frame_number, true);
this->add_page();
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);
const int orig_frame = scene.r.cfra;
for (int frame_number = scene.r.sfra; frame_number <= scene.r.efra; frame_number++) {
if (only_selected && !is_selected_frame(grease_pencil, frame_number)) {
continue;
}
scene.r.cfra = frame_number;
BKE_scene_graph_update_for_newframe(context_.depsgraph);
this->prepare_camera_params(scene, frame_number, true);
this->add_page();
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. */
Object *ob_eval = DEG_get_evaluated_object(context_.depsgraph, const_cast<Object *>(ob));
BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
const GreasePencil *grease_pencil_eval = static_cast<const GreasePencil *>(ob_eval->data);
for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
if (!layer->is_visible()) {
return;
}
const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
if (drawing == nullptr) {
return;
}
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);
const float4x4 viewmat = float4x4(context_.rv3d->viewmat);
const float4x4 layer_to_view = viewmat * layer_to_world;
auto write_stroke = [&](const Span<float3> positions,
const bool cyclic,
const ColorGeometry4f &color,
const float opacity,
const std::optional<float> width,
const bool /*round_cap*/,
const bool /*is_outline*/) {
write_stroke_to_polyline(layer_to_view, positions, cyclic, color, opacity, width);
};
foreach_stroke_in_layer(object, layer, drawing, write_stroke);
}
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;
}
bool PDFExporter::add_page()
{
page_ = HPDF_AddPage(pdf_);
if (!pdf_) {
std::cout << "error: cannot create PdfPage\n";
return false;
}
HPDF_Page_SetWidth(page_, render_size_.x);
HPDF_Page_SetHeight(page_, render_size_.y);
return true;
}
void PDFExporter::write_stroke_to_polyline(const float4x4 &transform,
const Span<float3> positions,
const bool cyclic,
const ColorGeometry4f &color,
const float opacity,
const std::optional<float> width)
{
if (width) {
HPDF_Page_SetLineJoin(page_, HPDF_ROUND_JOIN);
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(opacity, 0.0f, 1.0f));
HPDF_ExtGState_SetAlphaStroke(gstate, std::clamp(opacity, 0.0f, 1.0f));
}
}
else {
HPDF_Page_SetRGBFill(page_, srgb.r, srgb.g, srgb.b);
if (gstate) {
HPDF_ExtGState_SetAlphaFill(gstate, std::clamp(opacity, 0.0f, 1.0f));
}
}
if (gstate) {
HPDF_Page_SetExtGState(page_, gstate);
}
for (const int i : positions.index_range()) {
const float2 screen_co = this->project_to_screen(transform, positions[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) {
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,388 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_material.h"
#include "BLI_color.hh"
#include "BLI_math_matrix.hh"
#include "BLI_offset_indices.hh"
#include "BLI_string.h"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
#include "BKE_grease_pencil.hh"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "DEG_depsgraph_query.hh"
#include "DNA_view3d_types.h"
#include "GEO_resample_curves.hh"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "grease_pencil_io_intern.hh"
#include <fmt/core.h>
#include <fmt/format.h>
#include <numeric>
#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.0";
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 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");
}
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 {
public:
using GreasePencilExporter::GreasePencilExporter;
pugi::xml_document main_doc_;
bool 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_polygon(pugi::xml_node node,
const float4x4 &transform,
Span<float3> positions);
pugi::xml_node write_polyline(pugi::xml_node node,
const float4x4 &transform,
Span<float3> positions,
bool cyclic,
std::optional<float> width);
pugi::xml_node write_path(pugi::xml_node node,
const float4x4 &transform,
Span<float3> positions,
bool cyclic);
bool write_to_file(StringRefNull filepath);
};
bool SVGExporter::export_scene(Scene &scene, StringRefNull filepath)
{
const int frame_number = scene.r.cfra;
this->prepare_camera_params(scene, frame_number, false);
this->write_document_header();
pugi::xml_node main_node = this->write_main_node();
this->export_grease_pencil_objects(main_node, frame_number);
return this->write_to_file(filepath);
}
void SVGExporter::export_grease_pencil_objects(pugi::xml_node node, const int frame_number)
{
using bke::greasepencil::Drawing;
const bool is_clipping = is_camera_ && params_.use_clip_camera;
Vector<ObjectInfo> objects = retrieve_objects();
for (const ObjectInfo &info : objects) {
const Object *ob = info.object;
/* 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, render_size_.x, render_size_.y, 0.0f, "#000000");
}
pugi::xml_node frame_node = node.append_child("g");
std::string frametxt = "blender_frame_" + std::to_string(frame_number);
frame_node.append_attribute("id").set_value(frametxt.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());
}
pugi::xml_node ob_node = frame_node.append_child("g");
char obtxt[96];
SNPRINTF(obtxt, "blender_object_%s", ob->id.name + 2);
ob_node.append_attribute("id").set_value(obtxt);
/* Use evaluated version to get strokes with modifiers. */
Object *ob_eval = DEG_get_evaluated_object(context_.depsgraph, const_cast<Object *>(ob));
BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
const GreasePencil *grease_pencil_eval = static_cast<const GreasePencil *>(ob_eval->data);
for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
if (!layer->is_visible()) {
return;
}
const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
if (drawing == nullptr) {
return;
}
/* Layer node. */
const std::string txt = "Layer: " + layer->name();
node.append_child(pugi::node_comment).set_value(txt.c_str());
pugi::xml_node layer_node = node.append_child("g");
layer_node.append_attribute("id").set_value(layer->name().c_str());
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);
const float4x4 viewmat = float4x4(context_.rv3d->viewmat);
/* SVG has inverted Y axis. */
const float4x4 svg_coords = math::from_scale<float4x4>(float3(1, -1, 1));
const float4x4 layer_to_view = svg_coords * viewmat * layer_to_world;
auto write_stroke = [&](const Span<float3> positions,
const bool cyclic,
const ColorGeometry4f &color,
const float opacity,
const std::optional<float> width,
const bool round_cap,
const bool is_outline) {
if (is_outline) {
pugi::xml_node element_node = write_path(layer_node, layer_to_view, positions, cyclic);
write_fill_color_attribute(element_node, color, opacity);
}
else {
/* Fill is always exported as polygon because the stroke of the fill is done
* in a different SVG command. */
pugi::xml_node element_node = write_polyline(
layer_node, layer_to_view, positions, cyclic, width);
if (width) {
write_stroke_color_attribute(element_node, color, opacity, round_cap);
}
else {
write_fill_color_attribute(element_node, color, opacity);
}
}
};
foreach_stroke_in_layer(object, layer, drawing, write_stroke);
}
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.0");
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 = std::to_string(render_size_.x);
std::string height = std::to_string(render_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_polygon(pugi::xml_node node,
const float4x4 &transform,
const Span<float3> positions)
{
pugi::xml_node element_node = node.append_child("polygon");
std::string txt;
for (const int i : positions.index_range()) {
if (i > 0) {
txt.append(" ");
}
const float2 screen_co = this->project_to_screen(transform, positions[i]);
txt.append(std::to_string(screen_co.x) + "," + std::to_string(screen_co.y));
}
element_node.append_attribute("points").set_value(txt.c_str());
return element_node;
}
pugi::xml_node SVGExporter::write_polyline(pugi::xml_node node,
const float4x4 &transform,
const Span<float3> positions,
const bool cyclic,
const std::optional<float> width)
{
pugi::xml_node element_node = node.append_child(cyclic ? "polygon" : "polyline");
if (width) {
element_node.append_attribute("stroke-width").set_value(*width);
}
std::string txt;
for (const int i : positions.index_range()) {
if (i > 0) {
txt.append(" ");
}
const float2 screen_co = this->project_to_screen(transform, positions[i]);
txt.append(std::to_string(screen_co.x) + "," + std::to_string(screen_co.y));
}
element_node.append_attribute("points").set_value(txt.c_str());
return element_node;
}
pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
const float4x4 &transform,
const Span<float3> positions,
const bool cyclic)
{
pugi::xml_node element_node = node.append_child("path");
std::string txt = "M";
for (const int i : positions.index_range()) {
if (i > 0) {
txt.append("L");
}
const float2 screen_co = this->project_to_screen(transform, positions[i]);
txt.append(std::to_string(screen_co.x) + "," + std::to_string(screen_co.y));
}
/* Close patch (cyclic). */
if (cyclic) {
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;
}
bool 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,366 @@
/* 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.hh"
#include "BLI_math_color.h"
#include "BLI_math_euler_types.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_path_util.h"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_report.hh"
#include "BLI_string.h"
#include "DNA_grease_pencil_types.h"
#include "DNA_space_types.h"
#include "DNA_windowmanager_types.h"
#include "GEO_resample_curves.hh"
#include "ED_grease_pencil.hh"
#include "grease_pencil_io_intern.hh"
#include "nanosvg.h"
#include <fmt/core.h>
#include <fmt/format.h>
/** \file
* \ingroup bgrease_pencil
*/
using blender::bke::greasepencil::Drawing;
using blender::bke::greasepencil::Layer;
using blender::bke::greasepencil::TreeNode;
namespace blender::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;
}
/* 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 (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 ColorGeometry4f(0, 0, 0, 1);
case NSVG_PAINT_RADIAL_GRADIENT:
return ColorGeometry4f(0, 0, 0, 1);
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). */
const int point_num = (path->npts + 2) / 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();
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 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);
bke::SpanAttributeWriter fill_colors = attributes.lookup_or_add_for_write_span<ColorGeometry4f>(
"fill_color", bke::AttrDomain::Curve);
MutableSpan<bool> cyclic = curves.cyclic_for_write();
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);
fill_colors.span.slice(curves_range).fill(shape_color);
fill_opacities.span.slice(curves_range).fill(shape_color.a);
int curve_index = curves_range.start();
for (NSVGpath *path = shape.paths; path; path = path->next) {
if (path->npts == 0) {
continue;
}
cyclic[curve_index] = bool(path->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 IndexRange points = points_by_curve[curve_index];
for (const int i : points.index_range()) {
const int point_index = points[i];
const float2 pos_center = svg_path_data[i * 3];
const float2 pos_handle_left = (i > 0) ? svg_path_data[i * 3 - 1] : pos_center;
const float2 pos_handle_right = (i < points.size() - 1) ? 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;
const ColorGeometry4f point_color = convert_svg_color(shape.stroke);
vertex_colors.span[point_index] = point_color;
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];
BLI_strncpy(abs_filepath, filepath.c_str(), sizeof(abs_filepath));
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 = *static_cast<GreasePencil *>(object_->data);
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(-90, 0, 0)),
float3(svg_scale));
/* Loop all shapes. */
std::string prv_id = "*";
int prefix = 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;
}
}
/* Create materials. */
const bool is_fill = bool(shape->fill.type);
const bool is_stroke = bool(shape->stroke.type) || !is_fill;
const StringRefNull mat_name = (is_stroke ? (is_fill ? "Both" : "Stroke") : "Fill");
const int material_index = create_material(mat_name, is_stroke, is_fill);
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, material_index);
drawing->strokes_for_write() = std::move(curves);
}
/* 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);
}
return true;
}
bool import_svg(const IOContext &context, const ImportParams &params, StringRefNull filepath)
{
SVGImporter importer(context, params);
return importer.read(filepath);
}
} // namespace blender::io::grease_pencil

View File

@@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_color.hh"
#include "BLI_function_ref.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_vec_types.h"
#include "grease_pencil_io.hh"
#include <cstdint>
#include <optional>
#pragma once
/** \file
* \ingroup bgrease_pencil
*/
struct Scene;
struct Object;
struct GreasePencil;
struct Material;
struct RegionView3D;
namespace blender::bke::greasepencil {
class Layer;
class Drawing;
} // namespace blender::bke::greasepencil
namespace blender::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, bool stroke, bool fill);
};
class GreasePencilExporter {
public:
struct ObjectInfo {
Object *object;
float depth;
};
protected:
const IOContext context_;
const ExportParams params_;
/* Camera parameters. */
float4x4 persmat_;
int2 win_size_;
int2 render_size_;
bool is_camera_;
float camera_ratio_;
rctf camera_rect_;
float2 offset_;
public:
GreasePencilExporter(const IOContext &context, const ExportParams &params);
void prepare_camera_params(Scene &scene, int frame_number, bool force_camera_view);
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 WriteStrokeFn = FunctionRef<void(const Span<float3> positions,
bool cyclic,
const ColorGeometry4f &color,
float opacity,
std::optional<float> width,
bool round_cap,
bool is_outline)>;
void foreach_stroke_in_layer(const Object &object,
const bke::greasepencil::Layer &layer,
const bke::greasepencil::Drawing &drawing,
WriteStrokeFn stroke_fn);
float2 project_to_screen(const float4x4 &transform, const float3 &position) const;
};
} // namespace blender::io::grease_pencil

View File

@@ -328,8 +328,8 @@ if(WITH_IO_STL)
add_definitions(-DWITH_IO_STL)
endif()
if(WITH_IO_GPENCIL)
add_definitions(-DWITH_IO_GPENCIL)
if(WITH_IO_GREASE_PENCIL)
add_definitions(-DWITH_IO_GREASE_PENCIL)
endif()
if(WITH_ALEMBIC)

View File

@@ -263,7 +263,7 @@ static PyObject *make_builtopts_info()
SetObjIncref(Py_False);
#endif
#ifdef WITH_IO_GPENCIL
#ifdef WITH_IO_GREASE_PENCIL
SetObjIncref(Py_True);
#else
SetObjIncref(Py_False);