From 400c738db9be3e3a8a733cbc245ac3ee9a092271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Tue, 20 Aug 2024 11:41:37 +0200 Subject: [PATCH] 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 --- CMakeLists.txt | 2 +- build_files/cmake/config/blender_lite.cmake | 2 +- scripts/startup/bl_operators/presets.py | 1 - scripts/startup/bl_ui/space_topbar.py | 6 +- source/blender/editors/io/CMakeLists.txt | 20 +- .../{io_gpencil.hh => io_gpencil_legacy.hh} | 4 - ..._export.cc => io_gpencil_legacy_export.cc} | 6 +- ..._import.cc => io_gpencil_legacy_import.cc} | 19 +- ...il_utils.cc => io_gpencil_legacy_utils.cc} | 6 +- source/blender/editors/io/io_grease_pencil.cc | 597 ++++++++++++++++++ source/blender/editors/io/io_grease_pencil.hh | 27 + source/blender/editors/io/io_ops.cc | 10 +- source/blender/io/CMakeLists.txt | 7 +- .../CMakeLists.txt | 2 +- .../{gpencil => gpencil_legacy}/gpencil_io.h | 0 .../intern/gpencil_io_base.cc | 0 .../intern/gpencil_io_base.hh | 0 .../intern/gpencil_io_capi.cc | 0 .../intern/gpencil_io_export_base.hh | 0 .../intern/gpencil_io_export_pdf.cc | 0 .../intern/gpencil_io_export_pdf.hh | 0 .../intern/gpencil_io_export_svg.cc | 0 .../intern/gpencil_io_export_svg.hh | 0 .../intern/gpencil_io_import_base.cc | 0 .../intern/gpencil_io_import_base.hh | 0 .../intern/gpencil_io_import_svg.cc | 0 .../intern/gpencil_io_import_svg.hh | 0 .../blender/io/grease_pencil/CMakeLists.txt | 82 +++ .../io/grease_pencil/grease_pencil_io.hh | 86 +++ .../grease_pencil/intern/grease_pencil_io.cc | 578 +++++++++++++++++ .../intern/grease_pencil_io_export_pdf.cc | 288 +++++++++ .../intern/grease_pencil_io_export_svg.cc | 388 ++++++++++++ .../intern/grease_pencil_io_import_svg.cc | 366 +++++++++++ .../intern/grease_pencil_io_intern.hh | 103 +++ source/blender/python/intern/CMakeLists.txt | 4 +- .../python/intern/bpy_app_build_options.cc | 2 +- 36 files changed, 2556 insertions(+), 50 deletions(-) rename source/blender/editors/io/{io_gpencil.hh => io_gpencil_legacy.hh} (88%) rename source/blender/editors/io/{io_gpencil_export.cc => io_gpencil_legacy_export.cc} (99%) rename source/blender/editors/io/{io_gpencil_import.cc => io_gpencil_legacy_import.cc} (90%) rename source/blender/editors/io/{io_gpencil_utils.cc => io_gpencil_legacy_utils.cc} (90%) create mode 100644 source/blender/editors/io/io_grease_pencil.cc create mode 100644 source/blender/editors/io/io_grease_pencil.hh rename source/blender/io/{gpencil => gpencil_legacy}/CMakeLists.txt (95%) rename source/blender/io/{gpencil => gpencil_legacy}/gpencil_io.h (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_base.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_base.hh (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_capi.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_export_base.hh (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_export_pdf.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_export_pdf.hh (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_export_svg.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_export_svg.hh (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_import_base.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_import_base.hh (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_import_svg.cc (100%) rename source/blender/io/{gpencil => gpencil_legacy}/intern/gpencil_io_import_svg.hh (100%) create mode 100644 source/blender/io/grease_pencil/CMakeLists.txt create mode 100644 source/blender/io/grease_pencil/grease_pencil_io.hh create mode 100644 source/blender/io/grease_pencil/intern/grease_pencil_io.cc create mode 100644 source/blender/io/grease_pencil/intern/grease_pencil_io_export_pdf.cc create mode 100644 source/blender/io/grease_pencil/intern/grease_pencil_io_export_svg.cc create mode 100644 source/blender/io/grease_pencil/intern/grease_pencil_io_import_svg.cc create mode 100644 source/blender/io/grease_pencil/intern/grease_pencil_io_intern.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index 27de11815ab..7ce9389d72a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/build_files/cmake/config/blender_lite.cmake b/build_files/cmake/config/blender_lite.cmake index 6e19414ff98..3f4f1a9ca81 100644 --- a/build_files/cmake/config/blender_lite.cmake +++ b/build_files/cmake/config/blender_lite.cmake @@ -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) diff --git a/scripts/startup/bl_operators/presets.py b/scripts/startup/bl_operators/presets.py index 34d42ed77e7..6f7daa94785 100644 --- a/scripts/startup/bl_operators/presets.py +++ b/scripts/startup/bl_operators/presets.py @@ -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", diff --git a/scripts/startup/bl_ui/space_topbar.py b/scripts/startup/bl_ui/space_topbar.py index ac2bac0d103..85e47b94d46 100644 --- a/scripts/startup/bl_ui/space_topbar.py +++ b/scripts/startup/bl_ui/space_topbar.py @@ -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)") diff --git a/source/blender/editors/io/CMakeLists.txt b/source/blender/editors/io/CMakeLists.txt index 547d59e36a6..03275a7093b 100644 --- a/source/blender/editors/io/CMakeLists.txt +++ b/source/blender/editors/io/CMakeLists.txt @@ -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) diff --git a/source/blender/editors/io/io_gpencil.hh b/source/blender/editors/io/io_gpencil_legacy.hh similarity index 88% rename from source/blender/editors/io/io_gpencil.hh rename to source/blender/editors/io/io_gpencil_legacy.hh index 25ab46bf943..c7dbdda825b 100644 --- a/source/blender/editors/io/io_gpencil.hh +++ b/source/blender/editors/io/io_gpencil_legacy.hh @@ -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(); -} diff --git a/source/blender/editors/io/io_gpencil_export.cc b/source/blender/editors/io/io_gpencil_legacy_export.cc similarity index 99% rename from source/blender/editors/io/io_gpencil_export.cc rename to source/blender/editors/io/io_gpencil_legacy_export.cc index eb4956b401b..5581795f834 100644 --- a/source/blender/editors/io/io_gpencil_export.cc +++ b/source/blender/editors/io/io_gpencil_legacy_export.cc @@ -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 */ diff --git a/source/blender/editors/io/io_gpencil_import.cc b/source/blender/editors/io/io_gpencil_legacy_import.cc similarity index 90% rename from source/blender/editors/io/io_gpencil_import.cc rename to source/blender/editors/io/io_gpencil_legacy_import.cc index db15764d8d0..1a6abc1e3a0 100644 --- a/source/blender/editors/io/io_gpencil_import.cc +++ b/source/blender/editors/io/io_gpencil_legacy_import.cc @@ -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(); - 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 */ diff --git a/source/blender/editors/io/io_gpencil_utils.cc b/source/blender/editors/io/io_gpencil_legacy_utils.cc similarity index 90% rename from source/blender/editors/io/io_gpencil_utils.cc rename to source/blender/editors/io/io_gpencil_legacy_utils.cc index 95d69f13d79..70f32101cac 100644 --- a/source/blender/editors/io/io_gpencil_utils.cc +++ b/source/blender/editors/io/io_gpencil_legacy_utils.cc @@ -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 */ diff --git a/source/blender/editors/io/io_grease_pencil.cc b/source/blender/editors/io/io_grease_pencil.cc new file mode 100644 index 00000000000..aff04bb437a --- /dev/null +++ b/source/blender/editors/io/io_grease_pencil.cc @@ -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(area->spacedata.first); + *r_rv3d = static_cast(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, ®ion, &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, ®ion, &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, ®ion, &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(); + 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 */ diff --git a/source/blender/editors/io/io_grease_pencil.hh b/source/blender/editors/io/io_grease_pencil.hh new file mode 100644 index 00000000000..9c207a4e2f9 --- /dev/null +++ b/source/blender/editors/io/io_grease_pencil.hh @@ -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(); +} diff --git a/source/blender/editors/io/io_ops.cc b/source/blender/editors/io/io_ops.cc index 715c5548587..aa0d48402f1 100644 --- a/source/blender/editors/io/io_ops.cc +++ b/source/blender/editors/io/io_ops.cc @@ -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 diff --git a/source/blender/io/CMakeLists.txt b/source/blender/io/CMakeLists.txt index 37a85969d11..a66cb1e1b1e 100644 --- a/source/blender/io/CMakeLists.txt +++ b/source/blender/io/CMakeLists.txt @@ -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) diff --git a/source/blender/io/gpencil/CMakeLists.txt b/source/blender/io/gpencil_legacy/CMakeLists.txt similarity index 95% rename from source/blender/io/gpencil/CMakeLists.txt rename to source/blender/io/gpencil_legacy/CMakeLists.txt index a9bf90a7190..e6cca621721 100644 --- a/source/blender/io/gpencil/CMakeLists.txt +++ b/source/blender/io/gpencil_legacy/CMakeLists.txt @@ -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}") diff --git a/source/blender/io/gpencil/gpencil_io.h b/source/blender/io/gpencil_legacy/gpencil_io.h similarity index 100% rename from source/blender/io/gpencil/gpencil_io.h rename to source/blender/io/gpencil_legacy/gpencil_io.h diff --git a/source/blender/io/gpencil/intern/gpencil_io_base.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_base.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_base.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_base.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_base.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_base.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_base.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_base.hh diff --git a/source/blender/io/gpencil/intern/gpencil_io_capi.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_capi.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_capi.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_capi.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_base.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_export_base.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_export_base.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_export_base.hh diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_pdf.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_export_pdf.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_export_pdf.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_export_pdf.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_pdf.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_export_pdf.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_export_pdf.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_export_pdf.hh diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_svg.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_export_svg.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_export_svg.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_export_svg.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_svg.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_export_svg.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_export_svg.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_export_svg.hh diff --git a/source/blender/io/gpencil/intern/gpencil_io_import_base.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_import_base.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_import_base.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_import_base.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_import_base.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_import_base.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_import_base.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_import_base.hh diff --git a/source/blender/io/gpencil/intern/gpencil_io_import_svg.cc b/source/blender/io/gpencil_legacy/intern/gpencil_io_import_svg.cc similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_import_svg.cc rename to source/blender/io/gpencil_legacy/intern/gpencil_io_import_svg.cc diff --git a/source/blender/io/gpencil/intern/gpencil_io_import_svg.hh b/source/blender/io/gpencil_legacy/intern/gpencil_io_import_svg.hh similarity index 100% rename from source/blender/io/gpencil/intern/gpencil_io_import_svg.hh rename to source/blender/io/gpencil_legacy/intern/gpencil_io_import_svg.hh diff --git a/source/blender/io/grease_pencil/CMakeLists.txt b/source/blender/io/grease_pencil/CMakeLists.txt new file mode 100644 index 00000000000..c541f14fb2c --- /dev/null +++ b/source/blender/io/grease_pencil/CMakeLists.txt @@ -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}") diff --git a/source/blender/io/grease_pencil/grease_pencil_io.hh b/source/blender/io/grease_pencil/grease_pencil_io.hh new file mode 100644 index 00000000000..3e47e3010a0 --- /dev/null +++ b/source/blender/io/grease_pencil/grease_pencil_io.hh @@ -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 ¶ms, StringRefNull filepath); +bool 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 blender::io::grease_pencil diff --git a/source/blender/io/grease_pencil/intern/grease_pencil_io.cc b/source/blender/io/grease_pencil/intern/grease_pencil_io.cc new file mode 100644 index 00000000000..232df254241 --- /dev/null +++ b/source/blender/io/grease_pencil/intern/grease_pencil_io.cc @@ -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 + +/** \file + * \ingroup bgrease_pencil + */ + +namespace blender::io::grease_pencil { + +static float get_average(const Span values) +{ + return values.is_empty() ? 0.0f : + std::accumulate(values.begin(), values.end(), 0.0f) / values.size(); +} + +static ColorGeometry4f get_average(const Span values) +{ + if (values.is_empty()) { + return ColorGeometry4f(0); + } + /* ColorGeometry4f does not support arithmetic directly. */ + Span rgba_values = values.cast(); + float4 avg_rgba = std::accumulate(rgba_values.begin(), rgba_values.end(), float4(0)) / + values.size(); + return ColorGeometry4f(avg_rgba); +} + +static std::optional try_get_constant_value(const VArray values, + const float epsilon = 1e-5f) +{ + if (values.is_empty()) { + return std::nullopt; + } + const float first_value = values.first(); + const std::optional 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 /*value*/) -> std::optional { + for (const int i : range) { + if (math::abs(values[i] - first_value) > epsilon) { + return std::nullopt; + } + } + return first_value_opt; + }, + [&](const std::optional a, const std::optional 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 ¶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 = 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 ¶ms) + : 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 materials = *attributes.lookup(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), + 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> compute_drawing_bounds( + const ARegion ®ion, + 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> drawing_bounds = std::nullopt; + + BLI_assert(object.type == OB_GREASE_PENCIL); + GreasePencil &grease_pencil = *static_cast(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 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( + ®ion, 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> point_bounds = Bounds(pos_view); + point_bounds->pad(pixels); + drawing_bounds = bounds::merge(drawing_bounds, point_bounds); + } + } + }); + + return drawing_bounds; +} + +static std::optional> compute_objects_bounds( + const ARegion ®ion, + const RegionView3D &rv3d, + const Depsgraph &depsgraph, + const Span objects, + const int frame_number) +{ + using bke::greasepencil::Drawing; + using bke::greasepencil::Layer; + using ObjectInfo = GreasePencilExporter::ObjectInfo; + + constexpr float gap = 10.0f; + + std::optional> 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(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> 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(¶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()); + 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 objects = this->retrieve_objects(); + std::optional> 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 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 opacities) +{ + return get_average(opacities); +} + +std::optional GreasePencilExporter::try_get_uniform_point_width( + const RegionView3D &rv3d, const Span world_positions, const Span radii) +{ + VArray widths = VArray::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::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 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 cyclic = curves.cyclic(); + const VArraySpan material_indices = *attributes.lookup_or_default( + "material_index", bke::AttrDomain::Curve, 0); + const VArraySpan fill_colors = drawing.fill_colors(); + const VArray start_caps = *attributes.lookup_or_default( + "start_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_TYPE_ROUND); + const VArray end_caps = *attributes.lookup_or_default( + "end_cap", bke::AttrDomain::Curve, 0); + /* Point attributes. */ + const Span positions = curves.positions(); + const VArraySpan radii = drawing.radii(); + const VArraySpan opacities = drawing.opacities(); + const VArraySpan vertex_colors = drawing.vertex_colors(); + + Array 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), + 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 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 resample_lengths = VArray::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 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(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 diff --git a/source/blender/io/grease_pencil/intern/grease_pencil_io_export_pdf.cc b/source/blender/io/grease_pencil/intern/grease_pencil_io_export_pdf.cc new file mode 100644 index 00000000000..05c47fb31c4 --- /dev/null +++ b/source/blender/io/grease_pencil/intern/grease_pencil_io_export_pdf.cc @@ -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 + +/** \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 positions, + const bool cyclic, + const ColorGeometry4f &color, + const float opacity, + std::optional 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(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 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(ob)); + BLI_assert(ob_eval->type == OB_GREASE_PENCIL); + const GreasePencil *grease_pencil_eval = static_cast(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 positions, + const bool cyclic, + const ColorGeometry4f &color, + const float opacity, + const std::optional 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 positions, + const bool cyclic, + const ColorGeometry4f &color, + const float opacity, + const std::optional 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 ¶ms, + Scene &scene, + StringRefNull filepath) +{ + PDFExporter exporter(context, params); + return exporter.export_scene(scene, filepath); +} + +} // namespace blender::io::grease_pencil diff --git a/source/blender/io/grease_pencil/intern/grease_pencil_io_export_svg.cc b/source/blender/io/grease_pencil/intern/grease_pencil_io_export_svg.cc new file mode 100644 index 00000000000..a47e07e6cac --- /dev/null +++ b/source/blender/io/grease_pencil/intern/grease_pencil_io_export_svg.cc @@ -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 +#include +#include +#include +#include + +#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 positions); + pugi::xml_node write_polyline(pugi::xml_node node, + const float4x4 &transform, + Span positions, + bool cyclic, + std::optional width); + pugi::xml_node write_path(pugi::xml_node node, + const float4x4 &transform, + Span 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 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(ob)); + BLI_assert(ob_eval->type == OB_GREASE_PENCIL); + const GreasePencil *grease_pencil_eval = static_cast(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(float3(1, -1, 1)); + const float4x4 layer_to_view = svg_coords * viewmat * layer_to_world; + + auto write_stroke = [&](const Span positions, + const bool cyclic, + const ColorGeometry4f &color, + const float opacity, + const std::optional 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 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 positions, + const bool cyclic, + const std::optional 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 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 ¶ms, + Scene &scene, + StringRefNull filepath) +{ + SVGExporter exporter(context, params); + return exporter.export_scene(scene, filepath); +} + +} // namespace blender::io::grease_pencil diff --git a/source/blender/io/grease_pencil/intern/grease_pencil_io_import_svg.cc b/source/blender/io/grease_pencil/intern/grease_pencil_io_import_svg.cc new file mode 100644 index 00000000000..9cd34ae0be7 --- /dev/null +++ b/source/blender/io/grease_pencil/intern/grease_pencil_io_import_svg.cc @@ -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 +#include + +/** \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 old_offsets = curves.offsets(); + + /* Count curves and points. */ + Vector 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 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 materials = attributes.lookup_or_add_for_write_span( + "material_index", bke::AttrDomain::Curve); + bke::SpanAttributeWriter fill_colors = attributes.lookup_or_add_for_write_span( + "fill_color", bke::AttrDomain::Curve); + MutableSpan cyclic = curves.cyclic_for_write(); + bke::SpanAttributeWriter fill_opacities = attributes.lookup_or_add_for_write_span( + "fill_opacity", bke::AttrDomain::Curve); + + MutableSpan positions = curves.positions_for_write(); + MutableSpan handle_positions_left = curves.handle_positions_left_for_write(); + MutableSpan handle_positions_right = curves.handle_positions_right_for_write(); + MutableSpan handle_types_left = curves.handle_types_left_for_write(); + MutableSpan handle_types_right = curves.handle_types_right_for_write(); + bke::SpanAttributeWriter radii = attributes.lookup_or_add_for_write_span( + "radius", bke::AttrDomain::Point); + bke::SpanAttributeWriter vertex_colors = + attributes.lookup_or_add_for_write_span("vertex_color", + bke::AttrDomain::Point); + bke::SpanAttributeWriter point_opacities = attributes.lookup_or_add_for_write_span( + "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 svg_path_data = Span(path->pts, 2 * path->npts).cast(); + + 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 = [&]() { + std::optional> bounds; + for (GreasePencilDrawingBase *drawing_base : grease_pencil.drawings()) { + if (drawing_base->type != GP_DRAWING) { + continue; + } + Drawing &drawing = reinterpret_cast(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(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(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(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 ¶ms, StringRefNull filepath) +{ + SVGImporter importer(context, params); + return importer.read(filepath); +} + +} // namespace blender::io::grease_pencil diff --git a/source/blender/io/grease_pencil/intern/grease_pencil_io_intern.hh b/source/blender/io/grease_pencil/intern/grease_pencil_io_intern.hh new file mode 100644 index 00000000000..83f40aa1245 --- /dev/null +++ b/source/blender/io/grease_pencil/intern/grease_pencil_io_intern.hh @@ -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 +#include + +#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 ¶ms); + + 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 ¶ms); + + void prepare_camera_params(Scene &scene, int frame_number, bool force_camera_view); + + static ColorGeometry4f compute_average_stroke_color(const Material &material, + const Span vertex_colors); + static float compute_average_stroke_opacity(const Span opacities); + + /* Returns a value if point sizes are all equal. */ + static std::optional try_get_uniform_point_width(const RegionView3D &rv3d, + const Span world_positions, + const Span radii); + + Vector retrieve_objects() const; + + using WriteStrokeFn = FunctionRef positions, + bool cyclic, + const ColorGeometry4f &color, + float opacity, + std::optional 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 diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index 13c20e36b20..5914721f388 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -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) diff --git a/source/blender/python/intern/bpy_app_build_options.cc b/source/blender/python/intern/bpy_app_build_options.cc index c93ab2e9042..aa6a9356bc6 100644 --- a/source/blender/python/intern/bpy_app_build_options.cc +++ b/source/blender/python/intern/bpy_app_build_options.cc @@ -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);