From d77348ad9f4fb38e94624a9ca810fa293f51e531 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Wed, 5 Aug 2026 23:55:54 +0800 Subject: [PATCH] refactor(node): de-Qt oaknode and wrap it in a pure C ABI - copy engine/node (188 files) to src/node/src, de-Qt in waves: core infra (Node/Param/Value/Variant/mathtypes), project/serializer, block/output, color, effect leaves, generator, gizmo/plugins - strip QObject/signals/slots: notifications move to the facade's oakengine_event channel, ownership becomes explicit (unique_ptr, add_keyframe/add_gizmo), sender() replaced by current_gizmo - QVariant replaced by olive::Variant, Qt math types by POD mathtypes, QXmlStreamReader/Writer by oakcommon's expat-based classes - sink VideoParams/SubtitleParams/LoopMode/ColorTransform to oakcommon (M3.5); polygon/text rasterization behind backend hooks - pure C ABI in include/node + src/node/c_api (oaknode_ prefix, OAKNODE_E_* codes, undoable variants take OakUndoCommand out-params) - fix Project::clear() root_ reset + disconnect assert, Sequence TrackList leak - 96 gtest cases green in standalone build (build-oaknode) - docs: signal/slot handling strategy + M3 implementation status --- .github/workflows/ci.yml | 4 + docs/zh/plans/riir/M3-oaknode.md | 92 + docs/zh/plans/riir/notes.md | 87 + include/common/colortransform.h | 109 + include/common/loopmode.h | 44 + include/common/subtitleparams.h | 140 + include/common/videoparams.h | 301 ++ include/node/block.h | 240 ++ include/node/colormanager.h | 189 ++ include/node/error.h | 39 + include/node/factory.h | 96 + include/node/folder.h | 123 + include/node/footage.h | 199 ++ include/node/group.h | 163 + include/node/keyframe.h | 226 ++ include/node/node.h | 487 +++ include/node/project.h | 237 ++ include/node/sequence.h | 163 + include/node/serializer.h | 244 ++ include/node/track.h | 279 ++ include/node/traverser.h | 130 + src/CMakeLists.txt | 2 +- src/common/c_api/CMakeLists.txt | 3 + src/common/c_api/colortransform.cpp | 125 + src/common/c_api/subtitleparams.cpp | 223 ++ src/common/c_api/videoparams.cpp | 478 +++ src/common/src/CMakeLists.txt | 6 + src/common/src/colortransform.h | 91 + src/common/src/loopmode.h | 34 + src/common/src/subtitleparams.cpp | 172 ++ src/common/src/subtitleparams.h | 142 + src/common/src/videoparams.cpp | 466 +++ src/common/src/videoparams.h | 458 +++ src/common/tests/CMakeLists.txt | 3 + src/common/tests/colortransform_test.cpp | 121 + src/common/tests/subtitleparams_test.cpp | 251 ++ src/common/tests/videoparams_test.cpp | 525 ++++ src/node/CMakeLists.txt | 6 + src/node/DEQT.md | 313 ++ src/node/c_api/CMakeLists.txt | 15 + src/node/c_api/alivecount.h | 39 + src/node/c_api/block.cpp | 477 +++ src/node/c_api/colormanager.cpp | 369 +++ src/node/c_api/factory.cpp | 144 + src/node/c_api/folder.cpp | 232 ++ src/node/c_api/footage.cpp | 322 ++ src/node/c_api/group.cpp | 294 ++ src/node/c_api/keyframe.cpp | 588 ++++ src/node/c_api/node.cpp | 1030 +++++++ src/node/c_api/project.cpp | 373 +++ src/node/c_api/sequence.cpp | 319 ++ src/node/c_api/serializer.cpp | 357 +++ src/node/c_api/track.cpp | 574 ++++ src/node/c_api/traverser.cpp | 236 ++ src/node/c_api/valueconvert.h | 273 ++ src/node/src/CMakeLists.txt | 36 + src/node/src/audio/CMakeLists.txt | 23 + src/node/src/audio/pan/CMakeLists.txt | 22 + src/node/src/audio/pan/pan.cpp | 132 + src/node/src/audio/pan/pan.h | 60 + src/node/src/audio/volume/CMakeLists.txt | 22 + src/node/src/audio/volume/volume.cpp | 114 + src/node/src/audio/volume/volume.h | 56 + src/node/src/block/CMakeLists.txt | 27 + src/node/src/block/block.cpp | 144 + src/node/src/block/block.h | 133 + src/node/src/block/clip/CMakeLists.txt | 22 + src/node/src/block/clip/clip.cpp | 591 ++++ src/node/src/block/clip/clip.h | 279 ++ src/node/src/block/gap/CMakeLists.txt | 22 + src/node/src/block/gap/gap.cpp | 46 + src/node/src/block/gap/gap.h | 46 + src/node/src/block/subtitle/CMakeLists.txt | 22 + src/node/src/block/subtitle/subtitle.cpp | 73 + src/node/src/block/subtitle/subtitle.h | 57 + src/node/src/block/transition/CMakeLists.txt | 25 + .../transition/crossdissolve/CMakeLists.txt | 22 + .../crossdissolve/crossdissolvetransition.cpp | 97 + .../crossdissolve/crossdissolvetransition.h | 55 + .../transition/diptocolor/CMakeLists.txt | 22 + .../diptocolor/diptocolortransition.cpp | 82 + .../diptocolor/diptocolortransition.h | 55 + src/node/src/block/transition/transition.cpp | 343 +++ src/node/src/block/transition/transition.h | 120 + src/node/src/color/CMakeLists.txt | 29 + .../src/color/colormanager/CMakeLists.txt | 22 + .../src/color/colormanager/colormanager.cpp | 342 +++ .../src/color/colormanager/colormanager.h | 105 + .../src/color/displaytransform/CMakeLists.txt | 22 + .../displaytransform/displaytransform.cpp | 156 + .../color/displaytransform/displaytransform.h | 67 + src/node/src/color/ociobase/CMakeLists.txt | 22 + src/node/src/color/ociobase/ociobase.cpp | 78 + src/node/src/color/ociobase/ociobase.h | 71 + .../ociogradingtransformlinear/CMakeLists.txt | 22 + .../ociogradingtransformlinear.cpp | 300 ++ .../ociogradingtransformlinear.h | 83 + .../ociogradingtransformlog/CMakeLists.txt | 23 + .../ociogradingtransformlog.cpp | 295 ++ .../ociogradingtransformlog.h | 90 + src/node/src/color/ociolut/CMakeLists.txt | 14 + src/node/src/color/ociolut/ociolut.cpp | 315 ++ src/node/src/color/ociolut/ociolut.h | 85 + .../src/color/threewaycolor/CMakeLists.txt | 22 + .../src/color/threewaycolor/threewaycolor.cpp | 121 + .../src/color/threewaycolor/threewaycolor.h | 76 + .../src/color/whitebalance/CMakeLists.txt | 22 + .../src/color/whitebalance/whitebalance.cpp | 155 + .../src/color/whitebalance/whitebalance.h | 72 + src/node/src/distort/CMakeLists.txt | 30 + src/node/src/distort/cornerpin/CMakeLists.txt | 22 + .../cornerpin/cornerpindistortnode.cpp | 223 ++ .../distort/cornerpin/cornerpindistortnode.h | 95 + src/node/src/distort/crop/CMakeLists.txt | 22 + src/node/src/distort/crop/cropdistortnode.cpp | 191 ++ src/node/src/distort/crop/cropdistortnode.h | 92 + src/node/src/distort/flip/CMakeLists.txt | 22 + src/node/src/distort/flip/flipdistortnode.cpp | 99 + src/node/src/distort/flip/flipdistortnode.h | 55 + src/node/src/distort/mask/CMakeLists.txt | 22 + src/node/src/distort/mask/mask.cpp | 133 + src/node/src/distort/mask/mask.h | 70 + src/node/src/distort/ripple/CMakeLists.txt | 22 + .../src/distort/ripple/rippledistortnode.cpp | 137 + .../src/distort/ripple/rippledistortnode.h | 68 + src/node/src/distort/swirl/CMakeLists.txt | 22 + .../src/distort/swirl/swirldistortnode.cpp | 132 + src/node/src/distort/swirl/swirldistortnode.h | 66 + src/node/src/distort/tile/CMakeLists.txt | 22 + src/node/src/distort/tile/tiledistortnode.cpp | 180 ++ src/node/src/distort/tile/tiledistortnode.h | 80 + src/node/src/distort/transform/CMakeLists.txt | 22 + .../transform/transformdistortnode.cpp | 513 ++++ .../distort/transform/transformdistortnode.h | 147 + src/node/src/distort/wave/CMakeLists.txt | 22 + src/node/src/distort/wave/wavedistortnode.cpp | 106 + src/node/src/distort/wave/wavedistortnode.h | 57 + src/node/src/effect/CMakeLists.txt | 22 + src/node/src/effect/opacity/CMakeLists.txt | 22 + src/node/src/effect/opacity/opacityeffect.cpp | 98 + src/node/src/effect/opacity/opacityeffect.h | 73 + src/node/src/factory.cpp | 313 ++ src/node/src/factory.h | 125 + src/node/src/filter/CMakeLists.txt | 25 + src/node/src/filter/blur/CMakeLists.txt | 22 + src/node/src/filter/blur/blur.cpp | 237 ++ src/node/src/filter/blur/blur.h | 84 + src/node/src/filter/dropshadow/CMakeLists.txt | 22 + .../filter/dropshadow/dropshadowfilter.cpp | 107 + .../src/filter/dropshadow/dropshadowfilter.h | 71 + src/node/src/filter/mosaic/CMakeLists.txt | 22 + .../src/filter/mosaic/mosaicfilternode.cpp | 85 + src/node/src/filter/mosaic/mosaicfilternode.h | 70 + src/node/src/filter/stroke/CMakeLists.txt | 22 + src/node/src/filter/stroke/stroke.cpp | 116 + src/node/src/filter/stroke/stroke.h | 57 + src/node/src/generator/CMakeLists.txt | 27 + src/node/src/generator/matrix/CMakeLists.txt | 22 + src/node/src/generator/matrix/matrix.cpp | 161 + src/node/src/generator/matrix/matrix.h | 69 + src/node/src/generator/noise/CMakeLists.txt | 22 + src/node/src/generator/noise/noise.cpp | 101 + src/node/src/generator/noise/noise.h | 55 + src/node/src/generator/polygon/CMakeLists.txt | 22 + src/node/src/generator/polygon/polygon.cpp | 319 ++ src/node/src/generator/polygon/polygon.h | 89 + src/node/src/generator/shape/CMakeLists.txt | 26 + .../generator/shape/generatorwithmerge.cpp | 78 + .../src/generator/shape/generatorwithmerge.h | 48 + src/node/src/generator/shape/shapenode.cpp | 111 + src/node/src/generator/shape/shapenode.h | 60 + .../src/generator/shape/shapenodebase.cpp | 374 +++ src/node/src/generator/shape/shapenodebase.h | 81 + src/node/src/generator/solid/CMakeLists.txt | 22 + src/node/src/generator/solid/solid.cpp | 80 + src/node/src/generator/solid/solid.h | 53 + src/node/src/generator/text/CMakeLists.txt | 26 + src/node/src/generator/text/textbackend.h | 140 + src/node/src/generator/text/textv1.cpp | 206 ++ src/node/src/generator/text/textv1.h | 59 + src/node/src/generator/text/textv2.cpp | 231 ++ src/node/src/generator/text/textv2.h | 58 + src/node/src/generator/text/textv3.cpp | 360 +++ src/node/src/generator/text/textv3.h | 95 + src/node/src/geometry.h | 132 + src/node/src/gizmo/CMakeLists.txt | 36 + src/node/src/gizmo/draggable.cpp | 61 + src/node/src/gizmo/draggable.h | 85 + src/node/src/gizmo/gizmo.cpp | 42 + src/node/src/gizmo/gizmo.h | 78 + src/node/src/gizmo/line.cpp | 32 + src/node/src/gizmo/line.h | 79 + src/node/src/gizmo/path.cpp | 32 + src/node/src/gizmo/path.h | 44 + src/node/src/gizmo/point.cpp | 44 + src/node/src/gizmo/point.h | 77 + src/node/src/gizmo/polygon.cpp | 32 + src/node/src/gizmo/polygon.h | 54 + src/node/src/gizmo/screen.cpp | 32 + src/node/src/gizmo/screen.h | 37 + src/node/src/gizmo/text.cpp | 57 + src/node/src/gizmo/text.h | 126 + src/node/src/globals.cpp | 27 + src/node/src/globals.h | 95 + src/node/src/group/CMakeLists.txt | 22 + src/node/src/group/group.cpp | 391 +++ src/node/src/group/group.h | 156 + src/node/src/input/CMakeLists.txt | 24 + src/node/src/input/multicam/CMakeLists.txt | 22 + src/node/src/input/multicam/multicamnode.cpp | 206 ++ src/node/src/input/multicam/multicamnode.h | 102 + src/node/src/input/time/CMakeLists.txt | 22 + src/node/src/input/time/timeinput.cpp | 60 + src/node/src/input/time/timeinput.h | 47 + src/node/src/input/value/CMakeLists.txt | 22 + src/node/src/input/value/valuenode.cpp | 82 + src/node/src/input/value/valuenode.h | 74 + src/node/src/inputdragger.cpp | 163 + src/node/src/inputdragger.h | 84 + src/node/src/inputimmediate.cpp | 313 ++ src/node/src/inputimmediate.h | 194 ++ src/node/src/keyframe.cpp | 298 ++ src/node/src/keyframe.h | 225 ++ src/node/src/keying/CMakeLists.txt | 24 + src/node/src/keying/chromakey/CMakeLists.txt | 22 + src/node/src/keying/chromakey/chromakey.cpp | 182 ++ src/node/src/keying/chromakey/chromakey.h | 69 + .../keying/colordifferencekey/CMakeLists.txt | 22 + .../colordifferencekey/colordifferencekey.cpp | 113 + .../colordifferencekey/colordifferencekey.h | 54 + src/node/src/keying/despill/CMakeLists.txt | 22 + src/node/src/keying/despill/despill.cpp | 115 + src/node/src/keying/despill/despill.h | 52 + src/node/src/math/CMakeLists.txt | 24 + src/node/src/math/math/CMakeLists.txt | 24 + src/node/src/math/math/math.cpp | 126 + src/node/src/math/math/math.h | 71 + src/node/src/math/math/mathbase.cpp | 800 +++++ src/node/src/math/math/mathbase.h | 142 + src/node/src/math/merge/CMakeLists.txt | 22 + src/node/src/math/merge/merge.cpp | 104 + src/node/src/math/merge/merge.h | 59 + src/node/src/math/trigonometry/CMakeLists.txt | 22 + .../src/math/trigonometry/trigonometry.cpp | 120 + src/node/src/math/trigonometry/trigonometry.h | 65 + src/node/src/mathtypes.h | 975 ++++++ src/node/src/node.cpp | 2731 +++++++++++++++++ src/node/src/node.h | 1571 ++++++++++ src/node/src/nodeundo.cpp | 586 ++++ src/node/src/nodeundo.h | 770 +++++ src/node/src/output/CMakeLists.txt | 23 + src/node/src/output/track/CMakeLists.txt | 24 + src/node/src/output/track/track.cpp | 880 ++++++ src/node/src/output/track/track.h | 509 +++ src/node/src/output/track/tracklist.cpp | 173 ++ src/node/src/output/track/tracklist.h | 142 + src/node/src/output/viewer/CMakeLists.txt | 22 + src/node/src/output/viewer/viewer.cpp | 615 ++++ src/node/src/output/viewer/viewer.h | 293 ++ src/node/src/param.cpp | 183 ++ src/node/src/param.h | 365 +++ src/node/src/plugins/CMakeLists.txt | 22 + src/node/src/plugins/plugin.cpp | 683 +++++ src/node/src/plugins/plugin.h | 89 + src/node/src/project.cpp | 510 +++ src/node/src/project.h | 271 ++ src/node/src/project/CMakeLists.txt | 25 + src/node/src/project/folder/CMakeLists.txt | 23 + src/node/src/project/folder/folder.cpp | 183 ++ src/node/src/project/folder/folder.h | 211 ++ src/node/src/project/footage/CMakeLists.txt | 25 + src/node/src/project/footage/footage.cpp | 1054 +++++++ src/node/src/project/footage/footage.h | 359 +++ .../project/footage/footagedescription.cpp | 205 ++ .../src/project/footage/footagedescription.h | 216 ++ src/node/src/project/sequence/CMakeLists.txt | 23 + src/node/src/project/sequence/sequence.cpp | 221 ++ src/node/src/project/sequence/sequence.h | 131 + .../src/project/serializer/CMakeLists.txt | 43 + .../serializer/serializedlayoutinfo.cpp | 201 ++ .../project/serializer/serializedlayoutinfo.h | 71 + .../src/project/serializer/serializer.cpp | 391 +++ src/node/src/project/serializer/serializer.h | 293 ++ .../project/serializer/serializer190219.cpp | 34 + .../src/project/serializer/serializer190219.h | 46 + .../project/serializer/serializer210528.cpp | 876 ++++++ .../src/project/serializer/serializer210528.h | 106 + .../project/serializer/serializer210907.cpp | 865 ++++++ .../src/project/serializer/serializer210907.h | 105 + .../project/serializer/serializer211228.cpp | 919 ++++++ .../src/project/serializer/serializer211228.h | 106 + .../project/serializer/serializer220403.cpp | 1203 ++++++++ .../src/project/serializer/serializer220403.h | 117 + .../project/serializer/serializer230220.cpp | 577 ++++ .../src/project/serializer/serializer230220.h | 53 + .../src/project/serializer/typeserializer.cpp | 73 + .../src/project/serializer/typeserializer.h | 44 + src/node/src/serializeddata.cpp | 27 + src/node/src/serializeddata.h | 71 + src/node/src/sliderdisplaytype.h | 47 + src/node/src/splitvalue.h | 36 + src/node/src/time/CMakeLists.txt | 24 + src/node/src/time/timeformat/CMakeLists.txt | 22 + src/node/src/time/timeformat/timeformat.cpp | 195 ++ src/node/src/time/timeformat/timeformat.h | 53 + src/node/src/time/timeoffset/CMakeLists.txt | 22 + .../src/time/timeoffset/timeoffsetnode.cpp | 96 + src/node/src/time/timeoffset/timeoffsetnode.h | 78 + src/node/src/time/timeremap/CMakeLists.txt | 22 + src/node/src/time/timeremap/timeremap.cpp | 109 + src/node/src/time/timeremap/timeremap.h | 62 + src/node/src/traverser.cpp | 563 ++++ src/node/src/traverser.h | 242 ++ src/node/src/value.cpp | 495 +++ src/node/src/value.h | 544 ++++ src/node/src/valuedatabase.cpp | 43 + src/node/src/valuedatabase.h | 93 + src/node/src/variant.cpp | 314 ++ src/node/src/variant.h | 546 ++++ src/node/standalone/CMakeLists.txt | 91 + src/node/tests/CMakeLists.txt | 73 + src/node/tests/block_test.cpp | 307 ++ src/node/tests/colormanager_test.cpp | 298 ++ src/node/tests/factory_test.cpp | 149 + src/node/tests/folder_test.cpp | 192 ++ src/node/tests/footage_test.cpp | 172 ++ src/node/tests/group_test.cpp | 246 ++ src/node/tests/keyframe_test.cpp | 298 ++ src/node/tests/node_test.cpp | 601 ++++ src/node/tests/project_test.cpp | 266 ++ src/node/tests/sequence_test.cpp | 317 ++ src/node/tests/serializer_test.cpp | 242 ++ src/node/tests/testnode.h | 83 + src/node/tests/track_test.cpp | 339 ++ src/node/tests/traverser_test.cpp | 153 + src/node/transition/audio/audioprocessor.h | 15 + .../transition/audio/audiovisualwaveform.h | 2 + src/node/transition/codec/decoder.h | 17 + src/node/transition/codec/encoder.h | 7 + src/node/transition/codec/frame.h | 29 + src/node/transition/codec/proxymanager.h | 23 + src/node/transition/common/cancelableobject.h | 2 + src/node/transition/common/current.h | 7 + src/node/transition/common/lerp.h | 4 + src/node/transition/common/util.h | 4 + src/node/transition/config/config.h | 25 + src/node/transition/coreengine.h | 30 + src/node/transition/pluginSupport/olivehost.h | 12 + .../pluginSupport/oliveplugininstance.h | 30 + .../transition/render/audioplaybackcache.h | 14 + .../transition/render/audiowaveformcache.h | 14 + src/node/transition/render/cancelatom.h | 8 + src/node/transition/render/colorprocessor.h | 24 + src/node/transition/render/diskmanager.h | 9 + src/node/transition/render/framehashcache.h | 32 + .../transition/render/job/acceleratedjob.h | 23 + src/node/transition/render/job/cachejob.h | 10 + .../transition/render/job/colortransformjob.h | 16 + src/node/transition/render/job/footagejob.h | 27 + src/node/transition/render/job/generatejob.h | 11 + src/node/transition/render/job/pluginjob.h | 16 + src/node/transition/render/job/samplejob.h | 31 + src/node/transition/render/job/shaderjob.h | 25 + src/node/transition/render/lutlibrary.h | 15 + src/node/transition/render/playbackcache.h | 45 + .../transition/render/previewautocacher.h | 12 + src/node/transition/render/rendermanager.h | 12 + src/node/transition/render/samplebuffer.h | 6 + src/node/transition/render/shadercode.h | 9 + src/node/transition/render/texture.h | 26 + src/node/transition/timeline/timelinecommon.h | 22 + src/node/transition/timeline/timelinemarker.h | 25 + .../transition/timeline/timelineundogeneral.h | 15 + .../transition/timeline/timelineworkarea.h | 21 + src/node/transition/ui/colorcoding.h | 8 + 375 files changed, 59682 insertions(+), 1 deletion(-) create mode 100644 include/common/colortransform.h create mode 100644 include/common/loopmode.h create mode 100644 include/common/subtitleparams.h create mode 100644 include/common/videoparams.h create mode 100644 include/node/block.h create mode 100644 include/node/colormanager.h create mode 100644 include/node/error.h create mode 100644 include/node/factory.h create mode 100644 include/node/folder.h create mode 100644 include/node/footage.h create mode 100644 include/node/group.h create mode 100644 include/node/keyframe.h create mode 100644 include/node/node.h create mode 100644 include/node/project.h create mode 100644 include/node/sequence.h create mode 100644 include/node/serializer.h create mode 100644 include/node/track.h create mode 100644 include/node/traverser.h create mode 100644 src/common/c_api/colortransform.cpp create mode 100644 src/common/c_api/subtitleparams.cpp create mode 100644 src/common/c_api/videoparams.cpp create mode 100644 src/common/src/colortransform.h create mode 100644 src/common/src/loopmode.h create mode 100644 src/common/src/subtitleparams.cpp create mode 100644 src/common/src/subtitleparams.h create mode 100644 src/common/src/videoparams.cpp create mode 100644 src/common/src/videoparams.h create mode 100644 src/common/tests/colortransform_test.cpp create mode 100644 src/common/tests/subtitleparams_test.cpp create mode 100644 src/common/tests/videoparams_test.cpp create mode 100644 src/node/CMakeLists.txt create mode 100644 src/node/DEQT.md create mode 100644 src/node/c_api/CMakeLists.txt create mode 100644 src/node/c_api/alivecount.h create mode 100644 src/node/c_api/block.cpp create mode 100644 src/node/c_api/colormanager.cpp create mode 100644 src/node/c_api/factory.cpp create mode 100644 src/node/c_api/folder.cpp create mode 100644 src/node/c_api/footage.cpp create mode 100644 src/node/c_api/group.cpp create mode 100644 src/node/c_api/keyframe.cpp create mode 100644 src/node/c_api/node.cpp create mode 100644 src/node/c_api/project.cpp create mode 100644 src/node/c_api/sequence.cpp create mode 100644 src/node/c_api/serializer.cpp create mode 100644 src/node/c_api/track.cpp create mode 100644 src/node/c_api/traverser.cpp create mode 100644 src/node/c_api/valueconvert.h create mode 100644 src/node/src/CMakeLists.txt create mode 100644 src/node/src/audio/CMakeLists.txt create mode 100644 src/node/src/audio/pan/CMakeLists.txt create mode 100644 src/node/src/audio/pan/pan.cpp create mode 100644 src/node/src/audio/pan/pan.h create mode 100644 src/node/src/audio/volume/CMakeLists.txt create mode 100644 src/node/src/audio/volume/volume.cpp create mode 100644 src/node/src/audio/volume/volume.h create mode 100644 src/node/src/block/CMakeLists.txt create mode 100644 src/node/src/block/block.cpp create mode 100644 src/node/src/block/block.h create mode 100644 src/node/src/block/clip/CMakeLists.txt create mode 100644 src/node/src/block/clip/clip.cpp create mode 100644 src/node/src/block/clip/clip.h create mode 100644 src/node/src/block/gap/CMakeLists.txt create mode 100644 src/node/src/block/gap/gap.cpp create mode 100644 src/node/src/block/gap/gap.h create mode 100644 src/node/src/block/subtitle/CMakeLists.txt create mode 100644 src/node/src/block/subtitle/subtitle.cpp create mode 100644 src/node/src/block/subtitle/subtitle.h create mode 100644 src/node/src/block/transition/CMakeLists.txt create mode 100644 src/node/src/block/transition/crossdissolve/CMakeLists.txt create mode 100644 src/node/src/block/transition/crossdissolve/crossdissolvetransition.cpp create mode 100644 src/node/src/block/transition/crossdissolve/crossdissolvetransition.h create mode 100644 src/node/src/block/transition/diptocolor/CMakeLists.txt create mode 100644 src/node/src/block/transition/diptocolor/diptocolortransition.cpp create mode 100644 src/node/src/block/transition/diptocolor/diptocolortransition.h create mode 100644 src/node/src/block/transition/transition.cpp create mode 100644 src/node/src/block/transition/transition.h create mode 100644 src/node/src/color/CMakeLists.txt create mode 100644 src/node/src/color/colormanager/CMakeLists.txt create mode 100644 src/node/src/color/colormanager/colormanager.cpp create mode 100644 src/node/src/color/colormanager/colormanager.h create mode 100644 src/node/src/color/displaytransform/CMakeLists.txt create mode 100644 src/node/src/color/displaytransform/displaytransform.cpp create mode 100644 src/node/src/color/displaytransform/displaytransform.h create mode 100644 src/node/src/color/ociobase/CMakeLists.txt create mode 100644 src/node/src/color/ociobase/ociobase.cpp create mode 100644 src/node/src/color/ociobase/ociobase.h create mode 100644 src/node/src/color/ociogradingtransformlinear/CMakeLists.txt create mode 100644 src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.cpp create mode 100644 src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.h create mode 100644 src/node/src/color/ociogradingtransformlog/CMakeLists.txt create mode 100644 src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp create mode 100644 src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.h create mode 100644 src/node/src/color/ociolut/CMakeLists.txt create mode 100644 src/node/src/color/ociolut/ociolut.cpp create mode 100644 src/node/src/color/ociolut/ociolut.h create mode 100644 src/node/src/color/threewaycolor/CMakeLists.txt create mode 100644 src/node/src/color/threewaycolor/threewaycolor.cpp create mode 100644 src/node/src/color/threewaycolor/threewaycolor.h create mode 100644 src/node/src/color/whitebalance/CMakeLists.txt create mode 100644 src/node/src/color/whitebalance/whitebalance.cpp create mode 100644 src/node/src/color/whitebalance/whitebalance.h create mode 100644 src/node/src/distort/CMakeLists.txt create mode 100644 src/node/src/distort/cornerpin/CMakeLists.txt create mode 100644 src/node/src/distort/cornerpin/cornerpindistortnode.cpp create mode 100644 src/node/src/distort/cornerpin/cornerpindistortnode.h create mode 100644 src/node/src/distort/crop/CMakeLists.txt create mode 100644 src/node/src/distort/crop/cropdistortnode.cpp create mode 100644 src/node/src/distort/crop/cropdistortnode.h create mode 100644 src/node/src/distort/flip/CMakeLists.txt create mode 100644 src/node/src/distort/flip/flipdistortnode.cpp create mode 100644 src/node/src/distort/flip/flipdistortnode.h create mode 100644 src/node/src/distort/mask/CMakeLists.txt create mode 100644 src/node/src/distort/mask/mask.cpp create mode 100644 src/node/src/distort/mask/mask.h create mode 100644 src/node/src/distort/ripple/CMakeLists.txt create mode 100644 src/node/src/distort/ripple/rippledistortnode.cpp create mode 100644 src/node/src/distort/ripple/rippledistortnode.h create mode 100644 src/node/src/distort/swirl/CMakeLists.txt create mode 100644 src/node/src/distort/swirl/swirldistortnode.cpp create mode 100644 src/node/src/distort/swirl/swirldistortnode.h create mode 100644 src/node/src/distort/tile/CMakeLists.txt create mode 100644 src/node/src/distort/tile/tiledistortnode.cpp create mode 100644 src/node/src/distort/tile/tiledistortnode.h create mode 100644 src/node/src/distort/transform/CMakeLists.txt create mode 100644 src/node/src/distort/transform/transformdistortnode.cpp create mode 100644 src/node/src/distort/transform/transformdistortnode.h create mode 100644 src/node/src/distort/wave/CMakeLists.txt create mode 100644 src/node/src/distort/wave/wavedistortnode.cpp create mode 100644 src/node/src/distort/wave/wavedistortnode.h create mode 100644 src/node/src/effect/CMakeLists.txt create mode 100644 src/node/src/effect/opacity/CMakeLists.txt create mode 100644 src/node/src/effect/opacity/opacityeffect.cpp create mode 100644 src/node/src/effect/opacity/opacityeffect.h create mode 100644 src/node/src/factory.cpp create mode 100644 src/node/src/factory.h create mode 100644 src/node/src/filter/CMakeLists.txt create mode 100644 src/node/src/filter/blur/CMakeLists.txt create mode 100644 src/node/src/filter/blur/blur.cpp create mode 100644 src/node/src/filter/blur/blur.h create mode 100644 src/node/src/filter/dropshadow/CMakeLists.txt create mode 100644 src/node/src/filter/dropshadow/dropshadowfilter.cpp create mode 100644 src/node/src/filter/dropshadow/dropshadowfilter.h create mode 100644 src/node/src/filter/mosaic/CMakeLists.txt create mode 100644 src/node/src/filter/mosaic/mosaicfilternode.cpp create mode 100644 src/node/src/filter/mosaic/mosaicfilternode.h create mode 100644 src/node/src/filter/stroke/CMakeLists.txt create mode 100644 src/node/src/filter/stroke/stroke.cpp create mode 100644 src/node/src/filter/stroke/stroke.h create mode 100644 src/node/src/generator/CMakeLists.txt create mode 100644 src/node/src/generator/matrix/CMakeLists.txt create mode 100644 src/node/src/generator/matrix/matrix.cpp create mode 100644 src/node/src/generator/matrix/matrix.h create mode 100644 src/node/src/generator/noise/CMakeLists.txt create mode 100644 src/node/src/generator/noise/noise.cpp create mode 100644 src/node/src/generator/noise/noise.h create mode 100644 src/node/src/generator/polygon/CMakeLists.txt create mode 100644 src/node/src/generator/polygon/polygon.cpp create mode 100644 src/node/src/generator/polygon/polygon.h create mode 100644 src/node/src/generator/shape/CMakeLists.txt create mode 100644 src/node/src/generator/shape/generatorwithmerge.cpp create mode 100644 src/node/src/generator/shape/generatorwithmerge.h create mode 100644 src/node/src/generator/shape/shapenode.cpp create mode 100644 src/node/src/generator/shape/shapenode.h create mode 100644 src/node/src/generator/shape/shapenodebase.cpp create mode 100644 src/node/src/generator/shape/shapenodebase.h create mode 100644 src/node/src/generator/solid/CMakeLists.txt create mode 100644 src/node/src/generator/solid/solid.cpp create mode 100644 src/node/src/generator/solid/solid.h create mode 100644 src/node/src/generator/text/CMakeLists.txt create mode 100644 src/node/src/generator/text/textbackend.h create mode 100644 src/node/src/generator/text/textv1.cpp create mode 100644 src/node/src/generator/text/textv1.h create mode 100644 src/node/src/generator/text/textv2.cpp create mode 100644 src/node/src/generator/text/textv2.h create mode 100644 src/node/src/generator/text/textv3.cpp create mode 100644 src/node/src/generator/text/textv3.h create mode 100644 src/node/src/geometry.h create mode 100644 src/node/src/gizmo/CMakeLists.txt create mode 100644 src/node/src/gizmo/draggable.cpp create mode 100644 src/node/src/gizmo/draggable.h create mode 100644 src/node/src/gizmo/gizmo.cpp create mode 100644 src/node/src/gizmo/gizmo.h create mode 100644 src/node/src/gizmo/line.cpp create mode 100644 src/node/src/gizmo/line.h create mode 100644 src/node/src/gizmo/path.cpp create mode 100644 src/node/src/gizmo/path.h create mode 100644 src/node/src/gizmo/point.cpp create mode 100644 src/node/src/gizmo/point.h create mode 100644 src/node/src/gizmo/polygon.cpp create mode 100644 src/node/src/gizmo/polygon.h create mode 100644 src/node/src/gizmo/screen.cpp create mode 100644 src/node/src/gizmo/screen.h create mode 100644 src/node/src/gizmo/text.cpp create mode 100644 src/node/src/gizmo/text.h create mode 100644 src/node/src/globals.cpp create mode 100644 src/node/src/globals.h create mode 100644 src/node/src/group/CMakeLists.txt create mode 100644 src/node/src/group/group.cpp create mode 100644 src/node/src/group/group.h create mode 100644 src/node/src/input/CMakeLists.txt create mode 100644 src/node/src/input/multicam/CMakeLists.txt create mode 100644 src/node/src/input/multicam/multicamnode.cpp create mode 100644 src/node/src/input/multicam/multicamnode.h create mode 100644 src/node/src/input/time/CMakeLists.txt create mode 100644 src/node/src/input/time/timeinput.cpp create mode 100644 src/node/src/input/time/timeinput.h create mode 100644 src/node/src/input/value/CMakeLists.txt create mode 100644 src/node/src/input/value/valuenode.cpp create mode 100644 src/node/src/input/value/valuenode.h create mode 100644 src/node/src/inputdragger.cpp create mode 100644 src/node/src/inputdragger.h create mode 100644 src/node/src/inputimmediate.cpp create mode 100644 src/node/src/inputimmediate.h create mode 100644 src/node/src/keyframe.cpp create mode 100644 src/node/src/keyframe.h create mode 100644 src/node/src/keying/CMakeLists.txt create mode 100644 src/node/src/keying/chromakey/CMakeLists.txt create mode 100644 src/node/src/keying/chromakey/chromakey.cpp create mode 100644 src/node/src/keying/chromakey/chromakey.h create mode 100644 src/node/src/keying/colordifferencekey/CMakeLists.txt create mode 100644 src/node/src/keying/colordifferencekey/colordifferencekey.cpp create mode 100644 src/node/src/keying/colordifferencekey/colordifferencekey.h create mode 100644 src/node/src/keying/despill/CMakeLists.txt create mode 100644 src/node/src/keying/despill/despill.cpp create mode 100644 src/node/src/keying/despill/despill.h create mode 100644 src/node/src/math/CMakeLists.txt create mode 100644 src/node/src/math/math/CMakeLists.txt create mode 100644 src/node/src/math/math/math.cpp create mode 100644 src/node/src/math/math/math.h create mode 100644 src/node/src/math/math/mathbase.cpp create mode 100644 src/node/src/math/math/mathbase.h create mode 100644 src/node/src/math/merge/CMakeLists.txt create mode 100644 src/node/src/math/merge/merge.cpp create mode 100644 src/node/src/math/merge/merge.h create mode 100644 src/node/src/math/trigonometry/CMakeLists.txt create mode 100644 src/node/src/math/trigonometry/trigonometry.cpp create mode 100644 src/node/src/math/trigonometry/trigonometry.h create mode 100644 src/node/src/mathtypes.h create mode 100644 src/node/src/node.cpp create mode 100644 src/node/src/node.h create mode 100644 src/node/src/nodeundo.cpp create mode 100644 src/node/src/nodeundo.h create mode 100644 src/node/src/output/CMakeLists.txt create mode 100644 src/node/src/output/track/CMakeLists.txt create mode 100644 src/node/src/output/track/track.cpp create mode 100644 src/node/src/output/track/track.h create mode 100644 src/node/src/output/track/tracklist.cpp create mode 100644 src/node/src/output/track/tracklist.h create mode 100644 src/node/src/output/viewer/CMakeLists.txt create mode 100644 src/node/src/output/viewer/viewer.cpp create mode 100644 src/node/src/output/viewer/viewer.h create mode 100644 src/node/src/param.cpp create mode 100644 src/node/src/param.h create mode 100644 src/node/src/plugins/CMakeLists.txt create mode 100644 src/node/src/plugins/plugin.cpp create mode 100644 src/node/src/plugins/plugin.h create mode 100644 src/node/src/project.cpp create mode 100644 src/node/src/project.h create mode 100644 src/node/src/project/CMakeLists.txt create mode 100644 src/node/src/project/folder/CMakeLists.txt create mode 100644 src/node/src/project/folder/folder.cpp create mode 100644 src/node/src/project/folder/folder.h create mode 100644 src/node/src/project/footage/CMakeLists.txt create mode 100644 src/node/src/project/footage/footage.cpp create mode 100644 src/node/src/project/footage/footage.h create mode 100644 src/node/src/project/footage/footagedescription.cpp create mode 100644 src/node/src/project/footage/footagedescription.h create mode 100644 src/node/src/project/sequence/CMakeLists.txt create mode 100644 src/node/src/project/sequence/sequence.cpp create mode 100644 src/node/src/project/sequence/sequence.h create mode 100644 src/node/src/project/serializer/CMakeLists.txt create mode 100644 src/node/src/project/serializer/serializedlayoutinfo.cpp create mode 100644 src/node/src/project/serializer/serializedlayoutinfo.h create mode 100644 src/node/src/project/serializer/serializer.cpp create mode 100644 src/node/src/project/serializer/serializer.h create mode 100644 src/node/src/project/serializer/serializer190219.cpp create mode 100644 src/node/src/project/serializer/serializer190219.h create mode 100644 src/node/src/project/serializer/serializer210528.cpp create mode 100644 src/node/src/project/serializer/serializer210528.h create mode 100644 src/node/src/project/serializer/serializer210907.cpp create mode 100644 src/node/src/project/serializer/serializer210907.h create mode 100644 src/node/src/project/serializer/serializer211228.cpp create mode 100644 src/node/src/project/serializer/serializer211228.h create mode 100644 src/node/src/project/serializer/serializer220403.cpp create mode 100644 src/node/src/project/serializer/serializer220403.h create mode 100644 src/node/src/project/serializer/serializer230220.cpp create mode 100644 src/node/src/project/serializer/serializer230220.h create mode 100644 src/node/src/project/serializer/typeserializer.cpp create mode 100644 src/node/src/project/serializer/typeserializer.h create mode 100644 src/node/src/serializeddata.cpp create mode 100644 src/node/src/serializeddata.h create mode 100644 src/node/src/sliderdisplaytype.h create mode 100644 src/node/src/splitvalue.h create mode 100644 src/node/src/time/CMakeLists.txt create mode 100644 src/node/src/time/timeformat/CMakeLists.txt create mode 100644 src/node/src/time/timeformat/timeformat.cpp create mode 100644 src/node/src/time/timeformat/timeformat.h create mode 100644 src/node/src/time/timeoffset/CMakeLists.txt create mode 100644 src/node/src/time/timeoffset/timeoffsetnode.cpp create mode 100644 src/node/src/time/timeoffset/timeoffsetnode.h create mode 100644 src/node/src/time/timeremap/CMakeLists.txt create mode 100644 src/node/src/time/timeremap/timeremap.cpp create mode 100644 src/node/src/time/timeremap/timeremap.h create mode 100644 src/node/src/traverser.cpp create mode 100644 src/node/src/traverser.h create mode 100644 src/node/src/value.cpp create mode 100644 src/node/src/value.h create mode 100644 src/node/src/valuedatabase.cpp create mode 100644 src/node/src/valuedatabase.h create mode 100644 src/node/src/variant.cpp create mode 100644 src/node/src/variant.h create mode 100644 src/node/standalone/CMakeLists.txt create mode 100644 src/node/tests/CMakeLists.txt create mode 100644 src/node/tests/block_test.cpp create mode 100644 src/node/tests/colormanager_test.cpp create mode 100644 src/node/tests/factory_test.cpp create mode 100644 src/node/tests/folder_test.cpp create mode 100644 src/node/tests/footage_test.cpp create mode 100644 src/node/tests/group_test.cpp create mode 100644 src/node/tests/keyframe_test.cpp create mode 100644 src/node/tests/node_test.cpp create mode 100644 src/node/tests/project_test.cpp create mode 100644 src/node/tests/sequence_test.cpp create mode 100644 src/node/tests/serializer_test.cpp create mode 100644 src/node/tests/testnode.h create mode 100644 src/node/tests/track_test.cpp create mode 100644 src/node/tests/traverser_test.cpp create mode 100644 src/node/transition/audio/audioprocessor.h create mode 100644 src/node/transition/audio/audiovisualwaveform.h create mode 100644 src/node/transition/codec/decoder.h create mode 100644 src/node/transition/codec/encoder.h create mode 100644 src/node/transition/codec/frame.h create mode 100644 src/node/transition/codec/proxymanager.h create mode 100644 src/node/transition/common/cancelableobject.h create mode 100644 src/node/transition/common/current.h create mode 100644 src/node/transition/common/lerp.h create mode 100644 src/node/transition/common/util.h create mode 100644 src/node/transition/config/config.h create mode 100644 src/node/transition/coreengine.h create mode 100644 src/node/transition/pluginSupport/olivehost.h create mode 100644 src/node/transition/pluginSupport/oliveplugininstance.h create mode 100644 src/node/transition/render/audioplaybackcache.h create mode 100644 src/node/transition/render/audiowaveformcache.h create mode 100644 src/node/transition/render/cancelatom.h create mode 100644 src/node/transition/render/colorprocessor.h create mode 100644 src/node/transition/render/diskmanager.h create mode 100644 src/node/transition/render/framehashcache.h create mode 100644 src/node/transition/render/job/acceleratedjob.h create mode 100644 src/node/transition/render/job/cachejob.h create mode 100644 src/node/transition/render/job/colortransformjob.h create mode 100644 src/node/transition/render/job/footagejob.h create mode 100644 src/node/transition/render/job/generatejob.h create mode 100644 src/node/transition/render/job/pluginjob.h create mode 100644 src/node/transition/render/job/samplejob.h create mode 100644 src/node/transition/render/job/shaderjob.h create mode 100644 src/node/transition/render/lutlibrary.h create mode 100644 src/node/transition/render/playbackcache.h create mode 100644 src/node/transition/render/previewautocacher.h create mode 100644 src/node/transition/render/rendermanager.h create mode 100644 src/node/transition/render/samplebuffer.h create mode 100644 src/node/transition/render/shadercode.h create mode 100644 src/node/transition/render/texture.h create mode 100644 src/node/transition/timeline/timelinecommon.h create mode 100644 src/node/transition/timeline/timelinemarker.h create mode 100644 src/node/transition/timeline/timelineundogeneral.h create mode 100644 src/node/transition/timeline/timelineworkarea.h create mode 100644 src/node/transition/ui/colorcoding.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2693b52e9..6dff0b66c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,11 @@ name: CI on: push: + branches: + - main pull_request: + branches: + - main schedule: # Daily at 03:17 UTC to keep ccache/dependency caches warm - cron: '17 3 * * *' diff --git a/docs/zh/plans/riir/M3-oaknode.md b/docs/zh/plans/riir/M3-oaknode.md index 7eb6c550d..b23f1dd9a 100644 --- a/docs/zh/plans/riir/M3-oaknode.md +++ b/docs/zh/plans/riir/M3-oaknode.md @@ -80,3 +80,95 @@ M3 阶段判据(放宽版):oaknode 目录就位、C API 实现、oaknode_g - 枚举序数:NodeValue::Type ⇄ oak_node_value_type 映射表(已在 nodevaluehandle.h 钉过一次,oaknode 测试再钉一次,防两侧漂移)。 - `oaknode_debug_alive_count()` 泄漏断言。 + +## 实施现状(2026-08-05) + +M3 已落地并可独立构建、测试全绿(96 个用例全部通过,无 skip)。 +以下为与上文计划的实际差异。 + +### 最终目录结构 + +- `src/node/src/` — 去 Qt 化 C++ 实现(`olive::` 命名空间),target + `oaknode`(SHARED);平铺结构,`src/node/src` 为 include 根, + 模块内 include 无前缀(`"value.h"`、`"block/block.h"`)。 +- `src/node/c_api/` — 纯 C ABI 包装(node/group/keyframe/factory/ + traverser/project/folder/footage/serializer/block/track/sequence/ + colormanager 共 13 个 .cpp + 内部头 `valueconvert.h`/`alivecount.h`), + 通过 `target_sources` 合并进 `oaknode`,不单独成库。 +- `src/node/tests/` — gtest,单一 target `oaknode-gtest`(13 个 + _test.cpp + 共享夹具 `testnode.h`),`gtest_discover_tests` + (`DISCOVERY_MODE PRE_TEST`)。 +- `include/node/`(仓库根)— 公共 C 头:`error.h` + node/group/ + keyframe/factory/traverser/project/folder/footage/serializer/block/ + track/sequence/colormanager.h。 +- `src/node/standalone/CMakeLists.txt` — 独立构建 driver(见下)。 +- `src/node/DEQT.md` — 去Qt化替换约定与逐波次裁决记录。 +- `src/node/transition/` — 过渡 stub 头(见「实际依赖」)。 + +### 独立构建与测试 + +```sh +cmake -S src/node/standalone -B build-oaknode +cmake --build build-oaknode -j +ctest --test-dir build-oaknode --output-on-failure +``` + +driver 照 src/common/standalone 模式:EXPAT/OpenColorIO/OpenImageIO +用 Homebrew 的 config 包(`find_package(... CONFIG)`)并映射到 +`${OCIO_LIBRARIES}` 等变量;`add_subdirectory` 引入真实 in-repo +target(core→olivecore、ffmpeg_bridge、src/undo→oakundo、 +src/common→oakcommon,各自 BUILD_TESTS 关闭),不再链接预构建 +dylib;禁用 OpenTimelineIO(`/opt/otio` 的 `@loader_path` 问题, +oaknode 不需要)。 + +### 实际依赖 + +- Oak 内部:oakcommon(XML/Current/工具)、oakundo(UndoCommand/ + UndoStack)、olivecore(`olive::core::Rational/Color/Bezier` 等 + C ABI 包装,真实符号)、ffmpeg_bridge(经 oakcommon 间接)。 +- 第三方:EXPAT、OpenColorIO、OpenImageIO、Imath(头)、FFmpeg + (经 ffmpeg_bridge 间接)、GTest(仅测试)。 +- **transition stub 机制**(裁决 A):对尚未拆分的 + render/codec/timeline/audio/pluginSupport 模块的引用允许悬空—— + 头文件由 `src/node/transition/` 的过渡 stub/转发头提供(engine 头 + 仍是 Qt 版),符号经 `-undefined dynamic_lookup`(macOS)留到 + 运行时解析。测试进程启动时必须能解析这些符号:oaknode-gtest + 链接真实 target(olivecore/oakcommon/oakundo + OCIO/OIIO/Imath + dylib)并 `-Wl,-force_load` 预构建的 + `build/third_party/openfx/HostSupport/libOfxHost.a`(OFX 符号与 + typeinfo,否则二进制启动即崩,PRE_TEST 发现模式也会挂;路径用 + `find_library` 定位,可由 `OAKNODE_OFX_HOST_ARCHIVE` 覆盖)。 +- ColorManager 构造时需要有效 OCIO 配置(`:/ocioconf` qrc 提取是 + Qt 资源遗留、必然失败):ctest 经 `ENVIRONMENT OCIO=...` 指向 + `engine/render/ocioconf/config.ocio`,colormanager_test 另用 + `OAK_OCIO_TEST_CONFIG` 编译定义兜底。 + +### 与冻结 C API 的主要差异 + +- 函数族命名与约定照 oakcommon/oakundo 既有契约:`oaknode_<族>_<动词>`, + int 错误码 + out 参数,字符串两段式 buffer;undoable 变体成对 + (`_undoable` 后缀,部分经 `OakUndoCommand *` 尾参)。 +- §2 冻结表中**跳过/未实现**的函数族: + - Sequence 的 workarea/markers 族(timeline 边界类型 + TimelineMarker/TimelineWorkArea 未拆,留 M4); + - ProjectSerializer 的落盘 save/load(按计划迁 oakstorage M10), + 本模块只实现内存形态 `save_to_xml`/`load_from_xml` + SaveData/ + LoadData 句柄; + - Node 的 `oak_node_value` 句柄族未单独成族,输入值经 + `oaknode_node_get/set_input[_string][_undoable]` 直接收发; + - 无任何事件订阅接口(与 §2 特殊约定 2 一致)。 +- `oaknode_debug_alive_count()` 泄漏断言已实现并在测试中使用。 + +### 已知问题 + +- `NodeGroupAddInputPassthrough` 的上游 bug 原样保留(未修,待裁决)。 +- polygon/text 光栅化后端钩子(PathFillBackend/TextMeasureBackend/ + TextRenderBackend)未安装前输出空白;footage 离线警示帧丢失文字 + 叠层;track 默认高度固化为 13px——均为被迫行为差异,见 + `notes.md`「oaknode 去Qt化的删除与语义变更」。 +- 去Qt化顺带修了三个上游 bug(行为与旧版不同): + `Project::clear()` 重置 `root_`(可重新 initialize)、clear 删除 + 顺序修正(不再触发 disconnect_edge 的 parent assert)、 + `Sequence` 析构删除三个 `TrackList`(原泄漏)。详见 notes.md。 +- 库本体对 transition stub 模块的符号悬空在 M7/M9 复核前是预期 + 状态;禁止新增对 render/codec/timeline/audio/pluginSupport 的引用。 diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md index 2b8e662d5..d10a36d03 100644 --- a/docs/zh/plans/riir/notes.md +++ b/docs/zh/plans/riir/notes.md @@ -126,3 +126,90 @@ oakcommon;后续若确认无用途可直接删除。 转移、wrapper 被消费(不可再用/再 free); `oakundo_command_multi_child` 返回 borrowed wrapper(free 只释放 wrapper)。 + +## oaknode 去Qt化的删除与语义变更(2026-08-05) + +`src/node/`(Node 族、Project 族、Sequence/Track/Block 族、serializer、 +ColorManager)去Qt化过程中的删除与语义变化,迁移调用方时需注意 +(详细约定见 `src/node/DEQT.md` §4/§7): + +- 信号槽整组删除:Node 的 28 个信号(label_changed、input_connected、 + keyframe_added、added_to_graph 等)、NodeKeyframe 的 5 个、 + Project/Folder/Sequence/Footage 的全部信号(name_changed、 + modified_changed、track_added、begin/end_insert_item 等),变更通知 + 统一由 facade 经 `oakengine_event` 发出,oaknode 不持有上层回调。 +- QObject 父子生命周期改显式所有权:Project 持有全部节点 + (`add_node`/`remove_node`,`clear()`/析构删除);nodeundo 用 + `std::unique_ptr` 替代 `memory_manager_`;keyframe 经 + `Node::add_keyframe()/remove_keyframe()` 显式注册(自动 set_parent), + gizmo 经 `Node::add_gizmo()/remove_gizmo()` 显式注册(构造不再自 + 注册);`NodeInputImmediate::delete_all_keyframes(reclaimed)` 替代 + "reparent 续命"。 +- polygon/text 的 `generate_frame()` 光栅化(QPainter/QTextDocument) + 改后端钩子:`PathFillBackend`(geometry.h)、`TextMeasureBackend`/ + `TextRenderBackend`(textbackend.h),facade 未安装前输出空白 + (被迫行为差异)。 +- gizmo 绘制与 hit-test 归 app 层:各 gizmo 的 `draw(QPainter*)`、 + `PointGizmo::get_clicking_rect()/get_drawing_rect()` 等删除; + `DraggableGizmo` 的拖拽信号改直调 `parent_node()->gizmo_drag_*()`; + drag 回调里的 `sender()` 由 `Node::current_gizmo()` 替代。 +- footage 离线媒体警示帧:QImage/QPainter 改纯像素循环(深红底+斜纹), + "Media Offline" 文字叠层与抗锯齿丢失(被迫行为差异)。 +- `:/ocioconf` Qt 资源路径不可用:ColorManager 默认配置的 qrc 提取必然 + 失败,需 `OCIO` 环境变量指向磁盘 config.ocio(测试用 + `engine/render/ocioconf/config.ocio`)。 +- `NodeGroupAddInputPassthrough` 的上游 bug 原样保留(去Qt化不改行为 + 逻辑,待单独裁决修复)。 +- track.h 的 `QFontMetrics` 13px 常量:轨道默认高度原依赖 + `QFontMetrics(qApp->font()).height()`,去Qt后固化为 13px 字面量, + 换字体/DPI 不再自适应。 +- `Footage::check_footage()` 的 QTimer 周期回调删除,函数本体保留为 + public 由 facade 周期调用;`qApp->activeWindow()` 门槛移到 app 层。 +- 序列化 XML 元素/属性名与读写顺序逐字节不变;`XmlStreamWriter` 输出 + 紧凑 XML(无声明无缩进);OVEC 压缩段保持 qCompress 兼容 + (zlib + 4 字节大端长度)。 +- 修复三个上游真实 bug(行为因此与旧版不同,特此记录): + `Project::clear()` 现重置 `root_`(clear 后可再次 initialize); + clear 不再先 `set_parent(nullptr)` 再 delete(否则 ~Node 的 + disconnect_edge 触发 parent 不等的 assert); + `Sequence` 新增析构删除三个 `TrackList`(原来构造 new 后泄漏)。 +- `sender()` 的其他替代:原 keyframe signal→Node 的 5 条失效通知链 + (invalidate_from_keyframe_*)函数本体保留为 public 成员,带 + `NodeKeyframe *key` 显式参数,待 facade 接线。 + +## 信号与槽的处理策略(2026-08-05) + +去Qt化对 Qt 信号槽的统一处理模式,M1–M3 一致执行,后续模块照此: + +1. **模块内通知改显式回调/直调(std::function)**:通知双方都在同一 + 模块内时,去掉 connect,改为 `std::function` 回调注册或成员直调。 + 例:oakundo `UndoStack::index_changed(int)` signal → + `set_index_changed_callback(std::function)`;oaknode + `Node::current_gizmo()/set_current_gizmo()` 机制替代 gizmo drag + 回调里的 `sender()`;`DraggableGizmo` 的 handle_start/handle_movement + 信号改直调 `parent_node()->gizmo_drag_start()/gizmo_drag_move()`; + TrackList 原 track_list_changed/length_changed 发射点直调 + `sequence->update_track_cache()/verify_length()`。 +2. **跨层/跨界通知删除,由 facade 经 oakengine_event 统一发出**: + 一切指向 app/UI/其他模块的信号整组删除(Node 28 个、NodeKeyframe + 5 个、Project/Folder/Sequence/Footage 全部),oaknode 自身不持有 + 任何上层回调;facade 在执行命令后经既有 `oakengine_event` 通道发 + 通知(事件 id 沿用 `oakengine/events.h` 70-95 段,值不变)。依据: + oaknode 的所有修改都经命令函数完成,调用方知道影响(M3 §2 特殊 + 约定 2,04 §3)。 +3. **QObject 父子生命周期改显式所有权**:`setParent`/childEvent 机制 + 删除,所有权用显式容器与注册函数表达。例:Project 持有节点 + (`add_node`/`remove_node`/`clear()`);`ColorManager` 由 + `std::unique_ptr` 持有;nodeundo 用 `std::unique_ptr` 替代 + `memory_manager_`;keyframe/gizmo 分别经 `add_keyframe()`/ + `add_gizmo()` 显式注册;UndoCommand 树由 MultiUndoCommand 析构 + 删除 children。 +4. **sender() 的替代**:两种形态——拖曳场景用 + `Node::current_gizmo()`(DraggableGizmo 直调回调前后 + `set_current_gizmo(this/nullptr)` 包一层);通知链场景改显式参数, + 如 `invalidate_from_keyframe_*(NodeKeyframe *key)`。 +5. **原 slot 函数本体保留为 public 成员,待 facade 接线**:signal 删了 + 但 slot 承载的业务逻辑不删,改为 public 成员函数由上层直调。清单 + 见 `src/node/DEQT.md` §7(如 `Sequence::update_track_cache()`、 + `Footage::check_footage()/default_color_space_changed()/ + proxy_ready()/proxy_finished()`、5 条 invalidate_from_keyframe_*)。 diff --git a/include/common/colortransform.h b/include/common/colortransform.h new file mode 100644 index 000000000..3e7a1e7f9 --- /dev/null +++ b/include/common/colortransform.h @@ -0,0 +1,109 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_COLORTRANSFORM_H +#define OAK_EDITOR_COLORTRANSFORM_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a color transform description + * (olive::ColorTransform). + */ +typedef struct OakCommonColorTransform OakCommonColorTransform; + +/** + * @brief Create a plain output-colorspace transform. + * + * @param output Output colorspace name. Must not be NULL. + * @return Transform handle, or NULL on failure. + */ +OakCommonColorTransform *oakcommon_colortransform_init_output( + const char *output); + +/** + * @brief Create a display/view/look transform. + * + * All three strings must not be NULL. + * + * @return Transform handle, or NULL on failure. + */ +OakCommonColorTransform *oakcommon_colortransform_init_display( + const char *display, const char *view, const char *look); + +/** + * @brief Destroy a transform. No-op on NULL. + */ +void oakcommon_colortransform_free(OakCommonColorTransform *transform); + +/** + * @brief Query whether this is a display/view/look transform. + * + * @param is_display Receives the result. Must not be NULL. + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_colortransform_is_display(OakCommonColorTransform *transform, + int *is_display); + +/** + * @brief Get the display name (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_colortransform_get_display(OakCommonColorTransform *transform, + char *buf, int buf_size); + +/** + * @brief Get the output colorspace name (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_colortransform_get_output(OakCommonColorTransform *transform, + char *buf, int buf_size); + +/** + * @brief Get the view name (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_colortransform_get_view(OakCommonColorTransform *transform, + char *buf, int buf_size); + +/** + * @brief Get the look name (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_colortransform_get_look(OakCommonColorTransform *transform, + char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_COLORTRANSFORM_H diff --git a/include/common/loopmode.h b/include/common/loopmode.h new file mode 100644 index 000000000..79aa85205 --- /dev/null +++ b/include/common/loopmode.h @@ -0,0 +1,44 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_LOOPMODE_H +#define OAK_EDITOR_LOOPMODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Playback loop mode, mirroring olive::LoopMode. + * + * The numeric values must stay in sync with src/common/src/loopmode.h. + * Pure enum: no functions are needed. + */ +enum OakCommonLoopMode { + OAKCOMMON_LOOP_MODE_OFF = 0, /**< Looping disabled. */ + OAKCOMMON_LOOP_MODE_LOOP = 1, /**< Loop playback. */ + OAKCOMMON_LOOP_MODE_CLAMP = 2 /**< Clamp at the end. */ +}; + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_LOOPMODE_H diff --git a/include/common/subtitleparams.h b/include/common/subtitleparams.h new file mode 100644 index 000000000..ccd8cee5e --- /dev/null +++ b/include/common/subtitleparams.h @@ -0,0 +1,140 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_SUBTITLEPARAMS_H +#define OAK_EDITOR_SUBTITLEPARAMS_H + +#include "common/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a subtitle parameter set (olive::SubtitleParams). + */ +typedef struct OakCommonSubtitleParams OakCommonSubtitleParams; + +/** + * @brief Create an empty subtitle parameter set. + * + * @return Params handle, or NULL on allocation failure. + */ +OakCommonSubtitleParams *oakcommon_subtitleparams_init(void); + +/** + * @brief Destroy a subtitle parameter set. No-op on NULL. + */ +void oakcommon_subtitleparams_free(OakCommonSubtitleParams *params); + +int oakcommon_subtitleparams_get_stream_index( + OakCommonSubtitleParams *params, int *index); +int oakcommon_subtitleparams_set_stream_index( + OakCommonSubtitleParams *params, int index); +int oakcommon_subtitleparams_get_enabled(OakCommonSubtitleParams *params, + int *enabled); +int oakcommon_subtitleparams_set_enabled(OakCommonSubtitleParams *params, + int enabled); + +/** + * @brief Query whether the set contains at least one subtitle. + */ +int oakcommon_subtitleparams_is_valid(OakCommonSubtitleParams *params, + int *is_valid); + +/** + * @brief Number of subtitle entries. + */ +int oakcommon_subtitleparams_count(OakCommonSubtitleParams *params, + int *count); + +/** + * @brief Out time of the last subtitle (0/1 when empty). + */ +int oakcommon_subtitleparams_duration(OakCommonSubtitleParams *params, + int *numerator, int *denominator); + +/** + * @brief Append a subtitle entry. + * + * @param text Subtitle text. Must not be NULL. + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_add_subtitle(OakCommonSubtitleParams *params, + int in_num, int in_den, int out_num, + int out_den, const char *text); + +/** + * @brief Remove all subtitle entries. + */ +int oakcommon_subtitleparams_clear(OakCommonSubtitleParams *params); + +/** + * @brief Get the time range of the subtitle at @p index. + * + * @return OAKCOMMON_OK, OAKCOMMON_E_NOT_FOUND if @p index is out of range, + * or another negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_get_subtitle(OakCommonSubtitleParams *params, + int index, int *in_num, int *in_den, + int *out_num, int *out_den); + +/** + * @brief Get the text of the subtitle at @p index (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), OAKCOMMON_E_NOT_FOUND if @p index is out of + * range, or another negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_get_subtitle_text(OakCommonSubtitleParams *params, + int index, char *buf, + int buf_size); + +/** + * @brief Generate a default ASS header (static, no handle required). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_generate_ass_header(char *buf, int buf_size); + +/** + * @brief Load subtitles from an XML fragment. + * + * @param xml NUL-terminated XML text. Must not be NULL. + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params, + const char *xml); + +/** + * @brief Save subtitles to an XML fragment (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_subtitleparams_save_xml(OakCommonSubtitleParams *params, + char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_SUBTITLEPARAMS_H diff --git a/include/common/videoparams.h b/include/common/videoparams.h new file mode 100644 index 000000000..1d00a2381 --- /dev/null +++ b/include/common/videoparams.h @@ -0,0 +1,301 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_VIDEOPARAMS_H +#define OAK_EDITOR_VIDEOPARAMS_H + +#ifndef __cplusplus +#include +#endif + +#include + +#include "common/error.h" +#include "common/ocioutils.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a video parameter set (olive::VideoParams). + */ +typedef struct OakCommonVideoParams OakCommonVideoParams; + +/** + * @brief Interlacing modes, mirroring olive::VideoParams::Interlacing. + */ +enum OakCommonVideoInterlacing { + OAKCOMMON_VIDEO_INTERLACE_NONE = 0, + OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST = 1, + OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST = 2 +}; + +/** + * @brief Video stream types, mirroring olive::VideoParams::Type. + */ +enum OakCommonVideoType { + OAKCOMMON_VIDEO_TYPE_VIDEO = 0, + OAKCOMMON_VIDEO_TYPE_STILL = 1, + OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE = 2 +}; + +/** + * @brief Color range codes, mirroring olive::VideoParams::ColorRange. + */ +enum OakCommonVideoColorRange { + OAKCOMMON_COLOR_RANGE_LIMITED = 0, /**< 16-235 */ + OAKCOMMON_COLOR_RANGE_FULL = 1 /**< 0-255 */ +}; + +/** + * @brief Create a default (invalid) video parameter set. + * + * @return Params handle, or NULL on allocation failure. + */ +OakCommonVideoParams *oakcommon_videoparams_init(void); + +/** + * @brief Create a video parameter set without a time base. + * + * @param pixel_format One of the OakCommonPixelFormat values. + * @return Params handle, or NULL on allocation failure. + */ +OakCommonVideoParams *oakcommon_videoparams_init_basic( + int width, int height, int pixel_format, int nb_channels, + int pixel_aspect_num, int pixel_aspect_den, int interlacing, int divider); + +/** + * @brief Create a video parameter set with a time base. + * + * The frame rate is derived as the flipped time base. + * + * @param pixel_format One of the OakCommonPixelFormat values. + * @return Params handle, or NULL on allocation failure. + */ +OakCommonVideoParams *oakcommon_videoparams_init_with_time_base( + int width, int height, int time_base_num, int time_base_den, + int pixel_format, int nb_channels, int pixel_aspect_num, + int pixel_aspect_den, int interlacing, int divider); + +/** + * @brief Destroy a video parameter set. No-op on NULL. + */ +void oakcommon_videoparams_free(OakCommonVideoParams *params); + +int oakcommon_videoparams_get_width(OakCommonVideoParams *params, int *width); +int oakcommon_videoparams_set_width(OakCommonVideoParams *params, int width); +int oakcommon_videoparams_get_height(OakCommonVideoParams *params, int *height); +int oakcommon_videoparams_set_height(OakCommonVideoParams *params, int height); +int oakcommon_videoparams_get_depth(OakCommonVideoParams *params, int *depth); +int oakcommon_videoparams_set_depth(OakCommonVideoParams *params, int depth); +int oakcommon_videoparams_get_is_3d(OakCommonVideoParams *params, int *is_3d); + +/** + * @brief Rational getters return the value as a numerator/denominator pair. + */ +int oakcommon_videoparams_get_time_base(OakCommonVideoParams *params, + int *numerator, int *denominator); +int oakcommon_videoparams_set_time_base(OakCommonVideoParams *params, + int numerator, int denominator); +int oakcommon_videoparams_get_frame_rate(OakCommonVideoParams *params, + int *numerator, int *denominator); +int oakcommon_videoparams_set_frame_rate(OakCommonVideoParams *params, + int numerator, int denominator); +int oakcommon_videoparams_frame_rate_as_time_base(OakCommonVideoParams *params, + int *numerator, + int *denominator); +int oakcommon_videoparams_get_pixel_aspect_ratio(OakCommonVideoParams *params, + int *numerator, + int *denominator); +int oakcommon_videoparams_set_pixel_aspect_ratio(OakCommonVideoParams *params, + int numerator, int denominator); + +/** + * @brief Format getters/setters use the OakCommonPixelFormat codes. + */ +int oakcommon_videoparams_get_format(OakCommonVideoParams *params, int *format); +int oakcommon_videoparams_set_format(OakCommonVideoParams *params, int format); +int oakcommon_videoparams_get_channel_count(OakCommonVideoParams *params, + int *count); +int oakcommon_videoparams_set_channel_count(OakCommonVideoParams *params, + int count); +int oakcommon_videoparams_get_interlacing(OakCommonVideoParams *params, + int *interlacing); +int oakcommon_videoparams_set_interlacing(OakCommonVideoParams *params, + int interlacing); +int oakcommon_videoparams_get_divider(OakCommonVideoParams *params, + int *divider); +int oakcommon_videoparams_set_divider(OakCommonVideoParams *params, + int divider); +int oakcommon_videoparams_get_enabled(OakCommonVideoParams *params, + int *enabled); +int oakcommon_videoparams_set_enabled(OakCommonVideoParams *params, + int enabled); +int oakcommon_videoparams_get_x(OakCommonVideoParams *params, float *x); +int oakcommon_videoparams_set_x(OakCommonVideoParams *params, float x); +int oakcommon_videoparams_get_y(OakCommonVideoParams *params, float *y); +int oakcommon_videoparams_set_y(OakCommonVideoParams *params, float y); +int oakcommon_videoparams_get_stream_index(OakCommonVideoParams *params, + int *index); +int oakcommon_videoparams_set_stream_index(OakCommonVideoParams *params, + int index); +int oakcommon_videoparams_get_video_type(OakCommonVideoParams *params, + int *type); +int oakcommon_videoparams_set_video_type(OakCommonVideoParams *params, + int type); +int oakcommon_videoparams_get_start_time(OakCommonVideoParams *params, + int64_t *start_time); +int oakcommon_videoparams_set_start_time(OakCommonVideoParams *params, + int64_t start_time); +int oakcommon_videoparams_get_duration(OakCommonVideoParams *params, + int64_t *duration); +int oakcommon_videoparams_set_duration(OakCommonVideoParams *params, + int64_t duration); +int oakcommon_videoparams_get_premultiplied_alpha(OakCommonVideoParams *params, + int *premultiplied); +int oakcommon_videoparams_set_premultiplied_alpha(OakCommonVideoParams *params, + int premultiplied); +int oakcommon_videoparams_get_color_range(OakCommonVideoParams *params, + int *color_range); +int oakcommon_videoparams_set_color_range(OakCommonVideoParams *params, + int color_range); +int oakcommon_videoparams_get_color_primaries(OakCommonVideoParams *params, + int *primaries); +int oakcommon_videoparams_set_color_primaries(OakCommonVideoParams *params, + int primaries); +int oakcommon_videoparams_get_color_transfer(OakCommonVideoParams *params, + int *transfer); +int oakcommon_videoparams_set_color_transfer(OakCommonVideoParams *params, + int transfer); + +/** + * @brief Get the colorspace name (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_get_colorspace(OakCommonVideoParams *params, + char *buf, int buf_size); +int oakcommon_videoparams_set_colorspace(OakCommonVideoParams *params, + const char *colorspace); + +/** + * @brief Width multiplied by the pixel aspect ratio. + */ +int oakcommon_videoparams_get_square_pixel_width(OakCommonVideoParams *params, + int *width); +int oakcommon_videoparams_get_effective_width(OakCommonVideoParams *params, + int *width); +int oakcommon_videoparams_get_effective_height(OakCommonVideoParams *params, + int *height); +int oakcommon_videoparams_get_effective_depth(OakCommonVideoParams *params, + int *depth); +int oakcommon_videoparams_get_is_valid(OakCommonVideoParams *params, + int *is_valid); +int oakcommon_videoparams_get_bytes_per_channel(OakCommonVideoParams *params, + int *bytes); +int oakcommon_videoparams_get_bytes_per_pixel(OakCommonVideoParams *params, + int *bytes); +int oakcommon_videoparams_get_buffer_size(OakCommonVideoParams *params, + int *size); + +/** + * @brief Convert a time (in seconds, as a rational) to time base units. + * + * Returns INT64_MIN (AV_NOPTS_VALUE) in @p timestamp when no time base is + * set. + */ +int oakcommon_videoparams_get_time_in_timebase_units( + OakCommonVideoParams *params, int time_num, int time_den, + int64_t *timestamp); + +/** + * @brief Compare two parameter sets for equality. + */ +int oakcommon_videoparams_equals(OakCommonVideoParams *params, + OakCommonVideoParams *other, int *equal); + +/** + * @brief Load parameters from an XML fragment. + * + * @param xml NUL-terminated XML text. Must not be NULL. + * @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_load_xml(OakCommonVideoParams *params, + const char *xml); + +/** + * @brief Save parameters to an XML fragment (two-stage string getter). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_save_xml(OakCommonVideoParams *params, char *buf, + int buf_size); + +/* Static helpers (no handle required). */ + +int oakcommon_videoparams_get_bytes_per_channel_for_format(int pixel_format); +int oakcommon_videoparams_get_bytes_per_pixel_for_format(int pixel_format, + int channels); +int oakcommon_videoparams_calculate_buffer_size(int width, int height, + int pixel_format, + int channels); +int oakcommon_videoparams_format_is_float(int pixel_format); +int oakcommon_videoparams_generate_auto_divider(int64_t width, int64_t height); +int oakcommon_videoparams_get_scaled_dimension(int dimension, int divider); +int oakcommon_videoparams_get_divider_for_target_resolution(int src_width, + int src_height, + int dst_width, + int dst_height); + +/** + * @brief Human-readable name for a divider ("Full", "1/2", ...). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_get_name_for_divider(int divider, char *buf, + int buf_size); + +/** + * @brief Human-readable name for a pixel format. + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_get_format_name(int pixel_format, char *buf, + int buf_size); + +/** + * @brief Human-readable frame rate string ("23.976 FPS"). + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKCOMMON_E_* error code. + */ +int oakcommon_videoparams_frame_rate_to_string(int numerator, int denominator, + char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_VIDEOPARAMS_H diff --git a/include/node/block.h b/include/node/block.h new file mode 100644 index 000000000..55dc5fa78 --- /dev/null +++ b/include/node/block.h @@ -0,0 +1,240 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_BLOCK_H +#define OAK_EDITOR_NODE_BLOCK_H + +#ifndef __cplusplus +#include +#endif + +#include "node/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a timeline block (olive::Block). + * + * Covers the whole Block family: ClipBlock, GapBlock and the concrete + * TransitionBlock subclasses. The handle IS the C++ object pointer; no + * wrapper is allocated. Concrete instances are created through the + * oaknode_block_*_create() factories below; callers never touch C++ + * subclasses directly. + */ +typedef struct OakNodeBlock OakNodeBlock; + +/** + * @brief Opaque handle to a track (olive::Track), see node/track.h. + * + * Re-declared here so block.h is self-contained; the typedef is identical. + */ +typedef struct OakNodeTrack OakNodeTrack; + +/** + * @brief Concrete transition kinds for oaknode_block_transition_create(). + */ +enum OakNodeTransitionKind { + OAKNODE_TRANSITION_CROSS_DISSOLVE = 0, /**< CrossDissolveTransition. */ + OAKNODE_TRANSITION_DIP_TO_COLOR = 1 /**< DipToColorTransition. */ +}; + +/** + * @brief Create a ClipBlock. + * + * The caller owns the block until it is placed on a track that belongs to + * a project; a block that was never placed must be released with + * oaknode_block_free(). + * + * @return Block handle, or NULL on allocation failure. + */ +OakNodeBlock *oaknode_block_clip_create(void); + +/** + * @brief Create a GapBlock. Ownership as oaknode_block_clip_create(). + * + * @return Block handle, or NULL on allocation failure. + */ +OakNodeBlock *oaknode_block_gap_create(void); + +/** + * @brief Create a concrete TransitionBlock. + * + * @param kind One of the OakNodeTransitionKind values. + * @return Block handle, or NULL on invalid kind / allocation failure. + */ +OakNodeBlock *oaknode_block_transition_create(int kind); + +/** + * @brief Destroy a block. No-op on NULL. + * + * The block must not be placed on a track or linked to other nodes; the + * caller is responsible for detaching it first. + */ +void oaknode_block_free(OakNodeBlock *block); + +/** + * @brief Rational getters/setters use numerator/denominator out pairs. + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_block_get_in(OakNodeBlock *block, int *numerator, int *denominator); +int oaknode_block_set_in(OakNodeBlock *block, int numerator, int denominator); +int oaknode_block_get_out(OakNodeBlock *block, int *numerator, int *denominator); +int oaknode_block_set_out(OakNodeBlock *block, int numerator, int denominator); +int oaknode_block_get_length(OakNodeBlock *block, int *numerator, + int *denominator); + +/** + * @brief Set the block length, keeping the media out/in point anchored + * (olive::Block::set_length_and_media_out / _media_in). + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_block_set_length_and_media_out(OakNodeBlock *block, int numerator, + int denominator); +int oaknode_block_set_length_and_media_in(OakNodeBlock *block, int numerator, + int denominator); + +/** + * @brief Enabled flag (olive::Block::is_enabled/set_enabled). + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_block_get_enabled(OakNodeBlock *block, int *enabled); +int oaknode_block_set_enabled(OakNodeBlock *block, int enabled); + +/** + * @brief Adjacency accessors. `out` receives a borrowed handle (NULL when + * there is no neighbour / the block is not on a track). + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_block_get_previous(OakNodeBlock *block, OakNodeBlock **out); +int oaknode_block_get_next(OakNodeBlock *block, OakNodeBlock **out); +int oaknode_block_get_track(OakNodeBlock *block, OakNodeTrack **out); + +/** + * @brief Link two blocks (olive::Node::link/unlink/are_linked). + * + * Linked blocks move together in timeline edits. + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_FAILED (already + * linked / not linked). + */ +int oaknode_block_link(OakNodeBlock *a, OakNodeBlock *b); +int oaknode_block_unlink(OakNodeBlock *a, OakNodeBlock *b); +int oaknode_block_are_linked(OakNodeBlock *a, OakNodeBlock *b, int *linked); + +/** + * @brief Number of blocks linked to `block` (olive::Node::links()). + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_block_get_link_count(OakNodeBlock *block, int *count); + +/** + * @brief Borrowed handle to the linked block at `index`. + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. + */ +int oaknode_block_get_link_at(OakNodeBlock *block, int index, + OakNodeBlock **out); + +/* ---------------------------------------------------------------- Clip */ + +/** + * @brief Media in/out accessors (olive::ClipBlock). Non-clip blocks return + * OAKNODE_E_INVALID. + */ +int oaknode_clip_get_media_in(OakNodeBlock *clip, int *numerator, + int *denominator); +int oaknode_clip_set_media_in(OakNodeBlock *clip, int numerator, + int denominator); + +/** + * @brief Playback speed factor, 1.0 = normal (olive::ClipBlock speed input). + */ +int oaknode_clip_get_speed(OakNodeBlock *clip, double *speed); +int oaknode_clip_set_speed(OakNodeBlock *clip, double speed); + +/** + * @brief Reverse playback flag. + */ +int oaknode_clip_get_reverse(OakNodeBlock *clip, int *reverse); +int oaknode_clip_set_reverse(OakNodeBlock *clip, int reverse); + +/** + * @brief Maintain-audio-pitch flag. + */ +int oaknode_clip_get_maintain_audio_pitch(OakNodeBlock *clip, int *maintain); +int oaknode_clip_set_maintain_audio_pitch(OakNodeBlock *clip, int maintain); + +/** + * @brief Loop mode, one of the OakCommonLoopMode values + * (olive::ClipBlock::loop_mode/set_loop_mode). + */ +int oaknode_clip_get_loop_mode(OakNodeBlock *clip, int *loop_mode); +int oaknode_clip_set_loop_mode(OakNodeBlock *clip, int loop_mode); + +/** + * @brief Type of the track the clip sits on (OakNodeTrackType values, + * OAKNODE_TRACK_TYPE_NONE when trackless). + */ +int oaknode_clip_get_track_type(OakNodeBlock *clip, int *type); + +/* ----------------------------------------------------------- Transition */ + +/** + * @brief Transition offsets (olive::TransitionBlock). Non-transition blocks + * return OAKNODE_E_INVALID. + */ +int oaknode_transition_get_in_offset(OakNodeBlock *transition, int *numerator, + int *denominator); +int oaknode_transition_get_out_offset(OakNodeBlock *transition, int *numerator, + int *denominator); +int oaknode_transition_get_offset_center(OakNodeBlock *transition, + int *numerator, int *denominator); +int oaknode_transition_set_offset_center(OakNodeBlock *transition, + int numerator, int denominator); +int oaknode_transition_set_offsets_and_length(OakNodeBlock *transition, + int in_num, int in_den, + int out_num, int out_den); + +/** + * @brief Whether both sides of the transition are connected to clips. + */ +int oaknode_transition_is_dual(OakNodeBlock *transition, int *dual); + +/** + * @brief Borrowed handles to the connected out/in side blocks (NULL when + * unconnected). + */ +int oaknode_transition_get_connected_out_block(OakNodeBlock *transition, + OakNodeBlock **out); +int oaknode_transition_get_connected_in_block(OakNodeBlock *transition, + OakNodeBlock **out); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_BLOCK_H diff --git a/include/node/colormanager.h b/include/node/colormanager.h new file mode 100644 index 000000000..d89d6b4f1 --- /dev/null +++ b/include/node/colormanager.h @@ -0,0 +1,189 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_COLORMANAGER_H +#define OAK_EDITOR_NODE_COLORMANAGER_H + +#ifndef __cplusplus +#include +#endif + +#include "common/colortransform.h" +#include "node/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a color manager (olive::ColorManager). + * + * Unlike node handles this one IS a wrapper allocation (ColorManager is + * not a Node); release with oaknode_colormanager_free(). + */ +typedef struct OakNodeColorManager OakNodeColorManager; + +/** + * @brief Opaque borrowed handle to a project (olive::Project). + * + * Owned by the project family; re-declared here so this header is + * self-contained. + */ +typedef struct OakNodeProject OakNodeProject; + +/** + * @brief Create a color manager bound to `project` (borrowed). + * + * The manager is created without a config; call + * oaknode_colormanager_initialize() (or set a config filename and + * oaknode_colormanager_update_config_from_filename()) before using the + * config-dependent queries. + * + * @return Manager handle, or NULL on NULL project / allocation failure. + */ +OakNodeColorManager *oaknode_colormanager_init(OakNodeProject *project); + +/** + * @brief Destroy a color manager. No-op on NULL. + */ +void oaknode_colormanager_free(OakNodeColorManager *manager); + +/** + * @brief Load the built-in default OCIO config and set the default input + * colorspace (olive::ColorManager::init()). + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_FAILED (the OCIO + * config could not be created). + */ +int oaknode_colormanager_initialize(OakNodeColorManager *manager); + +/** + * @brief (Re)build the process-wide default OCIO config + * (olive::ColorManager::set_up_default_config()). + * + * @return OAKNODE_OK or OAKNODE_E_FAILED. + */ +int oaknode_colormanager_set_up_default_config(void); + +/** + * @brief Config filename stored on the project. Two-stage string getter: + * returns the required buffer size in bytes including NUL; pass + * buf == NULL or a too-small buffer to query the size. + */ +int oaknode_colormanager_get_config_filename(OakNodeColorManager *manager, + char *buf, int buf_size); +int oaknode_colormanager_set_config_filename(OakNodeColorManager *manager, + const char *filename); + +/** + * @brief Reload the OCIO config from the stored filename. Missing/invalid + * files are tolerated (the previous config is kept), matching + * olive::ColorManager::update_config_from_filename(). + */ +int oaknode_colormanager_update_config_from_filename( + OakNodeColorManager *manager); + +/** + * @brief Default input colorspace. Two-stage string accessor. + */ +int oaknode_colormanager_get_default_input_color_space( + OakNodeColorManager *manager, char *buf, int buf_size); +int oaknode_colormanager_set_default_input_color_space( + OakNodeColorManager *manager, const char *colorspace); + +/** + * @brief Reference (working) colorspace. Two-stage string getter. + */ +int oaknode_colormanager_get_reference_color_space( + OakNodeColorManager *manager, char *buf, int buf_size); + +/** + * @brief Return `colorspace` when the active config lists it, otherwise the + * default input colorspace. Two-stage string getter. Requires a config + * (OAKNODE_E_STATE when none is loaded). + */ +int oaknode_colormanager_get_compliant_color_space( + OakNodeColorManager *manager, const char *colorspace, char *buf, + int buf_size); + +/** + * @brief Map FFmpeg color primaries/transfer codes to a colorspace of the + * active config. Two-stage string getter; an empty result (required size + * 1) means "unknown tags, use the default". Requires a config + * (OAKNODE_E_STATE when none is loaded). + */ +int oaknode_colormanager_get_colorspace_for_ffmpeg_tags( + OakNodeColorManager *manager, int primaries, int trc, char *buf, + int buf_size); + +/** + * @brief Config listings. Count + per-index two-stage string getters. + * All require a loaded config (OAKNODE_E_STATE otherwise); index out of + * range yields OAKNODE_E_NOT_FOUND. + */ +int oaknode_colormanager_get_display_count(OakNodeColorManager *manager, + int *count); +int oaknode_colormanager_get_display_at(OakNodeColorManager *manager, + int index, char *buf, int buf_size); +int oaknode_colormanager_get_default_display(OakNodeColorManager *manager, + char *buf, int buf_size); +int oaknode_colormanager_get_view_count(OakNodeColorManager *manager, + const char *display, int *count); +int oaknode_colormanager_get_view_at(OakNodeColorManager *manager, + const char *display, int index, char *buf, + int buf_size); +int oaknode_colormanager_get_default_view(OakNodeColorManager *manager, + const char *display, char *buf, + int buf_size); +int oaknode_colormanager_get_look_count(OakNodeColorManager *manager, + int *count); +int oaknode_colormanager_get_look_at(OakNodeColorManager *manager, int index, + char *buf, int buf_size); +int oaknode_colormanager_get_colorspace_count(OakNodeColorManager *manager, + int *count); +int oaknode_colormanager_get_colorspace_at(OakNodeColorManager *manager, + int index, char *buf, + int buf_size); + +/** + * @brief Default luma coefficients of the active config into rgb[3]. + * Requires a loaded config (OAKNODE_E_STATE otherwise). + */ +int oaknode_colormanager_get_default_luma_coefs(OakNodeColorManager *manager, + double rgb[3]); + +/** + * @brief Return a copy of `transform` whose display/view/look (or output + * colorspace) is clamped to what the active config offers + * (olive::ColorManager::get_compliant_color_space(ColorTransform, bool)). + * + * `out` receives a NEW handle owned by the caller (release with + * oakcommon_colortransform_free()). Requires a loaded config + * (OAKNODE_E_STATE otherwise). + */ +int oaknode_colormanager_get_compliant_color_transform( + OakNodeColorManager *manager, const OakCommonColorTransform *transform, + int force_display, OakCommonColorTransform **out); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_COLORMANAGER_H diff --git a/include/node/error.h b/include/node/error.h new file mode 100644 index 000000000..c9a0b9857 --- /dev/null +++ b/include/node/error.h @@ -0,0 +1,39 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_ERROR_H +#define OAK_EDITOR_NODE_ERROR_H + +/** + * @brief Status and error codes shared by all oaknode C API families. + * + * Return-code convention (mirrors engine/include/oakengine/init.h): + * 0 (OAKNODE_OK) on success, a negative OAKNODE_E_* error code on + * failure. String getters return the required buffer size in bytes + * (including the terminating NUL) as a non-negative value instead. + */ +#define OAKNODE_OK 0 /**< Success. */ +#define OAKNODE_E_INVALID (-1) /**< NULL handle or invalid argument. */ +#define OAKNODE_E_STATE (-2) /**< Call not valid in the current state. */ +#define OAKNODE_E_FAILED (-3) /**< The underlying operation failed. */ +#define OAKNODE_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */ +#define OAKNODE_E_NOMEM (-5) /**< Allocation failed. */ + +#endif //OAK_EDITOR_NODE_ERROR_H diff --git a/include/node/factory.h b/include/node/factory.h new file mode 100644 index 000000000..d27aef920 --- /dev/null +++ b/include/node/factory.h @@ -0,0 +1,96 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_FACTORY_H +#define OAK_EDITOR_NODE_FACTORY_H + +#include "node/error.h" +#include "node/node.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file factory.h + * @brief C ABI for olive::NodeFactory (src/node/src/factory.h): the + * internal node-type library. + * + * The library must be populated with oaknode_factory_initialize() before + * any other call; oaknode_factory_destroy() releases it. Prototype nodes + * from oaknode_factory_node_at() are owned by the library: read-only + * metadata queries only, never free them or add them to a graph. + */ + +/** + * @brief Populate the internal node library (NodeFactory::initialize()). + * Idempotent: calling twice is a no-op. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_factory_initialize(void); + +/** + * @brief Release the internal node library (NodeFactory::destroy()). + * Safe when not initialized. + */ +void oaknode_factory_destroy(void); + +/** + * @brief Number of registered node types (the library size). + * OAKNODE_E_STATE when not initialized. + */ +int oaknode_factory_id_count(int *out_count); + +/** + * @brief The type id of the registered node at `index`. Two-stage + * getter; OAKNODE_E_NOT_FOUND for an out-of-range index, + * OAKNODE_E_STATE when not initialized. + */ +int oaknode_factory_id_at(int index, char *buf, int buf_size); + +/** + * @brief The display name of the node type `type_id` + * (NodeFactory::get_name_from_id()). Two-stage getter; an unknown id + * yields an empty string (required size 1). + */ +int oaknode_factory_name_from_id(const char *type_id, char *buf, + int buf_size); + +/** + * @brief Create a node of `type_id` WITHOUT adding it to any graph + * (NodeFactory::create_from_id()). The caller owns the returned node + * and must release it with oaknode_node_free() while it is still + * orphaned. Returns NULL when the id is unknown or not initialized. + */ +OakNodeNode *oaknode_factory_create_from_id(const char *type_id); + +/** + * @brief Borrow the prototype node at `index` in the library. + * OAKNODE_E_NOT_FOUND for an out-of-range index, OAKNODE_E_STATE when + * not initialized. + */ +int oaknode_factory_node_at(int index, OakNodeNode **out_node); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_FACTORY_H diff --git a/include/node/folder.h b/include/node/folder.h new file mode 100644 index 000000000..c10e4705b --- /dev/null +++ b/include/node/folder.h @@ -0,0 +1,123 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_FOLDER_H +#define OAK_EDITOR_NODE_FOLDER_H + +#include "node/error.h" +#include "node/project.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file folder.h + * @brief C ABI for olive::Folder (oaknode) + * + * A folder is a project node that organizes item children (footage, + * sequences, subfolders). Folder handles are borrowed from the owning + * project; they become invalid when the project is freed or cleared. + * + * Child add/remove/move operations execute the underlying undo commands + * live (redo_now); wiring them onto an undo stack is the oakundo / + * facade layer's job, not this layer's. + */ + +/** + * @brief Create a folder node owned by `project`. + * + * The folder is added to the project's graph (Project::add_node()) but is + * NOT attached under any parent folder; use oaknode_folder_add_child() to + * place it. The handle is borrowed: the project owns the folder. + * + * @return Folder handle, or NULL on failure. + */ +OakNodeFolder *oaknode_folder_create(OakNodeProject *project); + +/** + * @brief Number of direct item children (Folder::item_child_count()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_folder_child_count(const OakNodeFolder *folder); + +/** + * @brief Borrowed node handle of the item child at `index` + * (Folder::item_child()). NULL when out of range. + */ +OakNodeNode *oaknode_folder_child_at(const OakNodeFolder *folder, int index); + +/** + * @brief Add `child` as a direct item child of `folder` (live, non-undoable; + * executes FolderAddChild::redo()). + * + * @return OAKNODE_OK, OAKNODE_E_STATE if `child` already belongs to a + * folder, or another negative OAKNODE_E_* error code. + */ +int oaknode_folder_add_child(OakNodeFolder *folder, OakNodeNode *child); + +/** + * @brief Remove `child` from `folder` without deleting it (live, + * non-undoable; executes Folder::RemoveElementCommand::redo()). + * + * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND if `child` is not a direct child, + * or another negative OAKNODE_E_* error code. + */ +int oaknode_folder_remove_child(OakNodeFolder *folder, OakNodeNode *child); + +/** + * @brief Move several nodes into `dest_folder` (live, non-undoable). + * + * Each node is removed from its current folder (if any) and appended to + * `dest_folder`. Nodes already directly inside `dest_folder` are skipped. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_folder_move_children(OakNodeNode *const *nodes, int count, + OakNodeFolder *dest_folder); + +/** + * @brief 1 if `folder` recursively contains `child`, 0 otherwise + * (Folder::has_child_recursive()). Negative OAKNODE_E_* code on NULL args. + */ +int oaknode_folder_has_child_recursive(const OakNodeFolder *folder, + const OakNodeNode *child); + +/** + * @brief Index of `child` in `folder`'s direct children + * (Folder::index_of_child()). + * + * @return The index, OAKNODE_E_NOT_FOUND if not a direct child, or + * OAKNODE_E_INVALID on NULL args. + */ +int oaknode_folder_index_of_child(const OakNodeFolder *folder, + const OakNodeNode *child); + +/** + * @brief Borrowed handle of the folder a node currently belongs to + * (Node::folder()), or NULL if the node is not in any folder. + */ +OakNodeFolder *oaknode_folder_parent_of(const OakNodeNode *node); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_FOLDER_H diff --git a/include/node/footage.h b/include/node/footage.h new file mode 100644 index 000000000..5e11b66d1 --- /dev/null +++ b/include/node/footage.h @@ -0,0 +1,199 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_FOOTAGE_H +#define OAK_EDITOR_NODE_FOOTAGE_H + +#include + +#include "node/error.h" +#include "node/project.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file footage.h + * @brief C ABI for olive::Footage (oaknode) + * + * A footage node references an external media file and caches its stream + * metadata. Footage handles are borrowed from the owning project; they + * become invalid when the project is freed or cleared. + * + * NOTE: setting a filename whose file exists on disk triggers a probe, + * which requires the codec/render modules (outside oaknode). Tests and + * pure-graph consumers should use nonexistent paths; probing is the + * facade layer's job. + */ + +/** + * @brief Opaque footage handle. Borrowed from the owning project. + */ +typedef struct OakNodeFootage OakNodeFootage; + +/** + * @brief Create a footage node owned by `project` (added to the project's + * graph, not attached to any folder). + * + * @param filename Initial media path, may be NULL/empty. + * + * @return Footage handle, or NULL on failure. + */ +OakNodeFootage *oaknode_footage_create(OakNodeProject *project, + const char *filename); + +/** + * @brief Current media path (Footage::filename()). Two-stage string getter. + * + * @return Required buffer size in bytes including the NUL, or a negative + * OAKNODE_E_* error code. + */ +int oaknode_footage_filename(const OakNodeFootage *footage, char *buf, + int buf_size); + +/** + * @brief Set the media path (Footage::set_filename()). Does not re-probe + * unless the file exists (see the file comment above). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_set_filename(OakNodeFootage *footage, const char *filename); + +/** + * @brief 1 if the footage was successfully probed and is ready for use + * (Footage::is_valid()), 0 otherwise. Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_is_valid(const OakNodeFootage *footage); + +/** + * @brief Last-modified timestamp of the media file in milliseconds since the + * epoch (Footage::timestamp()). + * + * @param out_timestamp Receives the timestamp. Must not be NULL. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_timestamp(const OakNodeFootage *footage, + int64_t *out_timestamp); + +/** + * @brief Set the last-modified timestamp (Footage::set_timestamp()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_set_timestamp(OakNodeFootage *footage, int64_t timestamp); + +/** + * @brief Decoder ID recorded when the footage was probed + * (Footage::decoder()). Two-stage string getter. + */ +int oaknode_footage_decoder(const OakNodeFootage *footage, char *buf, + int buf_size); + +/** + * @brief Total number of streams (Footage::get_total_stream_count()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_total_stream_count(const OakNodeFootage *footage); + +/** + * @brief Number of video streams (ViewerOutput::get_video_stream_count()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_video_stream_count(const OakNodeFootage *footage); + +/** + * @brief Number of audio streams (ViewerOutput::get_audio_stream_count()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_audio_stream_count(const OakNodeFootage *footage); + +/** + * @brief Number of subtitle streams (ViewerOutput::get_subtitle_stream_count()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_subtitle_stream_count(const OakNodeFootage *footage); + +/** + * @brief Footage duration as a rational number of seconds + * (ViewerOutput::get_length()). + * + * @param out_numerator Receives the numerator. Must not be NULL. + * @param out_denominator Receives the denominator. Must not be NULL. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_duration(const OakNodeFootage *footage, + int *out_numerator, int *out_denominator); + +/** + * @brief 1 if proxy playback is enabled (Footage::proxy_enabled()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_proxy_enabled(const OakNodeFootage *footage); + +/** + * @brief Enable/disable proxy playback (Footage::set_proxy_enabled()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_set_proxy_enabled(OakNodeFootage *footage, int enabled); + +/** + * @brief Proxy file path, or "" when none (Footage::proxy_path()). + * Two-stage string getter. + */ +int oaknode_footage_proxy_path(const OakNodeFootage *footage, char *buf, + int buf_size); + +/** + * @brief Proxy state enum value (Footage::proxy_state(): + * ProxyManager::ProxyState). Negative OAKNODE_E_* code on NULL. + */ +int oaknode_footage_proxy_state(const OakNodeFootage *footage); + +/** + * @brief Set all proxy fields at once (Footage::set_proxy()). + * + * @param path Proxy file path, may be NULL/empty. + * @param state ProxyManager::ProxyState enum value. + * @param video_stream_index Proxy's video stream index (-1 when none). + * @param preset_version Proxy preset version. + * @param enabled Non-zero to enable proxy playback. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_set_proxy(OakNodeFootage *footage, const char *path, + int state, int video_stream_index, + int preset_version, int enabled); + +/** + * @brief Clear all proxy fields (Footage::clear_proxy()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_footage_clear_proxy(OakNodeFootage *footage); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_FOOTAGE_H diff --git a/include/node/group.h b/include/node/group.h new file mode 100644 index 000000000..f87e50fa4 --- /dev/null +++ b/include/node/group.h @@ -0,0 +1,163 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_GROUP_H +#define OAK_EDITOR_NODE_GROUP_H + +#include "node/error.h" +#include "node/node.h" +#include "undo/undocommand.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file group.h + * @brief C ABI for olive::NodeGroup (src/node/src/group/group.h): + * input passthrough management and input resolution. + * + * An OakNodeGroup is a reinterpreted olive::NodeGroup (a Node subclass); + * group handles borrow the same lifetime rules as OakNodeNode. + */ + +/** + * @brief Opaque group handle (olive::NodeGroup). + */ +typedef struct OakNodeGroup OakNodeGroup; + +/** + * @brief Create a standalone NodeGroup (owned; release with + * oaknode_node_free() on the OakNodeNode view or oaknode_group_free() + * while still orphaned). + * + * @return Group handle, or NULL on allocation failure. + */ +OakNodeGroup *oaknode_group_create(void); + +/** + * @brief Borrow a group view of a node, or NULL when the node is not a + * NodeGroup (dynamic_cast). + */ +OakNodeGroup *oaknode_group_cast(OakNodeNode *node); + +/** + * @brief Destroy an OWNED group (same rules as oaknode_node_free()). + * NULL is a no-op. + */ +void oaknode_group_free(OakNodeGroup *group); + +/** + * @brief Add an input passthrough for (`node`, `input_id`, `element`) + * (live, NodeGroup::add_input_passthrough()). The generated passthrough + * id is returned through the two-stage string convention. + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKNODE_E_* error code. + */ +int oaknode_group_add_input_passthrough(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, int element, + char *buf, int buf_size); + +/** + * @brief Create an add-passthrough command + * (olive::NodeGroupAddInputPassthrough). The generated id is NOT + * retrievable through this call (the command computes it on redo). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_group_add_input_passthrough_undoable(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, + int element, + OakUndoCommand **out_command); + +/** + * @brief Remove the passthrough for (`node`, `input_id`, `element`) + * (live). OAKNODE_E_NOT_FOUND when no such passthrough exists. + */ +int oaknode_group_remove_input_passthrough(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, int element); + +/** + * @brief Number of registered input passthroughs. + */ +int oaknode_group_passthrough_count(const OakNodeGroup *group, int *out_count); + +/** + * @brief The passthrough id at `index`. Two-stage getter; + * OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_group_passthrough_id_at(const OakNodeGroup *group, int index, + char *buf, int buf_size); + +/** + * @brief The inner input behind passthrough `index`: node (borrowed + * handle), input id (two-stage string) and element. + * OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_group_passthrough_input_at(const OakNodeGroup *group, int index, + OakNodeNode **out_node, char *buf, + int buf_size, int *out_element); + +/** + * @brief The output passthrough node (borrowed handle), or NULL when + * unset. OAKNODE_OK is returned either way. + */ +int oaknode_group_get_output_passthrough(const OakNodeGroup *group, + OakNodeNode **out_node); + +/** + * @brief Set the output passthrough node directly (live). + */ +int oaknode_group_set_output_passthrough(OakNodeGroup *group, + OakNodeNode *node); + +/** + * @brief Create a set-output-passthrough command + * (olive::NodeGroupSetOutputPassthrough). + */ +int oaknode_group_set_output_passthrough_undoable( + OakNodeGroup *group, OakNodeNode *node, OakUndoCommand **out_command); + +/** + * @brief Resolve an input through group passthroughs + * (NodeGroup::resolve_input()): follows a group's passthrough id to the + * inner node input. Non-group inputs resolve to themselves. + * + * `out_node` (may be NULL) receives a borrowed handle; the resolved input + * id uses the two-stage string convention; `out_element` (may be NULL) + * receives the element. OAKNODE_E_NOT_FOUND when the input does not + * resolve to a valid target. + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKNODE_E_* error code. + */ +int oaknode_group_resolve_input(OakNodeNode *node, const char *input_id, + int element, OakNodeNode **out_node, + char *buf, int buf_size, int *out_element); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_GROUP_H diff --git a/include/node/keyframe.h b/include/node/keyframe.h new file mode 100644 index 000000000..dde58b4fa --- /dev/null +++ b/include/node/keyframe.h @@ -0,0 +1,226 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_KEYFRAME_H +#define OAK_EDITOR_NODE_KEYFRAME_H + +#include + +#include "node/error.h" +#include "node/node.h" +#include "undo/undocommand.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file keyframe.h + * @brief C ABI for olive::NodeKeyframe (src/node/src/keyframe.h). + * + * An OakNodeKeyframe is a reinterpreted olive::NodeKeyframe. Handles + * created by oaknode_keyframe_create() are owned and must be released + * with oaknode_keyframe_free(); keyframes attached to a node input's + * track are owned by the node. + * + * Every setter comes in a live variant and an undoable variant (suffix + * _undoable) returning an owned, un-executed OakUndoCommand. + */ + +/** + * @brief Interpolation type of a keyframe (olive::NodeKeyframe::Type). + */ +typedef enum oaknode_keyframe_type { + OAKNODE_KEYFRAME_INVALID = -1, + OAKNODE_KEYFRAME_LINEAR = 0, + OAKNODE_KEYFRAME_HOLD = 1, + OAKNODE_KEYFRAME_BEZIER = 2 +} oaknode_keyframe_type; + +/** + * @brief Bezier handle selector (olive::NodeKeyframe::BezierType). + */ +typedef enum oaknode_keyframe_bezier { + OAKNODE_KEYFRAME_IN_HANDLE = 0, + OAKNODE_KEYFRAME_OUT_HANDLE = 1 +} oaknode_keyframe_bezier; + +/** + * @brief Opaque keyframe handle (olive::NodeKeyframe). + */ +typedef struct OakNodeKeyframe OakNodeKeyframe; + +/** + * @brief Create a standalone keyframe (owned; release with + * oaknode_keyframe_free()). + * + * `value` may be NULL (null variant); OAKNODE_VALUE_STRING is rejected + * (use oaknode_keyframe_set_value_string() after creation). `type` is an + * oaknode_keyframe_type. `parent_or_null` may be NULL. + * + * @return Keyframe handle, or NULL on invalid argument or allocation + * failure. + */ +OakNodeKeyframe *oaknode_keyframe_create(int64_t time_num, int64_t time_den, + const oaknode_value *value, int type, + int track, int element, + const char *input_id, + OakNodeNode *parent_or_null); + +/** + * @brief Destroy an OWNED keyframe. NULL is a no-op. Never free a + * keyframe that is attached to a node's track. + */ +void oaknode_keyframe_free(OakNodeKeyframe *keyframe); + +/** + * @brief The keyframe's time as a rational (numerator/denominator). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_keyframe_get_time(const OakNodeKeyframe *keyframe, + int64_t *out_num, int64_t *out_den); + +/** + * @brief Set the keyframe's time directly (live). + */ +int oaknode_keyframe_set_time(OakNodeKeyframe *keyframe, int64_t time_num, + int64_t time_den); + +/** + * @brief Create a set-time command (olive::NodeParamSetKeyframeTimeCommand). + */ +int oaknode_keyframe_set_time_undoable(OakNodeKeyframe *keyframe, + int64_t time_num, int64_t time_den, + OakUndoCommand **out_command); + +/** + * @brief Read the keyframe's value mapped into `out`. Values without a + * POD representation fail with OAKNODE_E_FAILED. + */ +int oaknode_keyframe_get_value(const OakNodeKeyframe *keyframe, + oaknode_value *out); + +/** + * @brief Set the keyframe's value directly (live). + * OAKNODE_VALUE_STRING is rejected (use + * oaknode_keyframe_set_value_string()). + */ +int oaknode_keyframe_set_value(OakNodeKeyframe *keyframe, + const oaknode_value *v); + +/** + * @brief Create a set-value command + * (olive::NodeParamSetKeyframeValueCommand). + */ +int oaknode_keyframe_set_value_undoable(OakNodeKeyframe *keyframe, + const oaknode_value *v, + OakUndoCommand **out_command); + +/** + * @brief Read a string value. Two-stage getter. + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKNODE_E_* error code. + */ +int oaknode_keyframe_get_value_string(const OakNodeKeyframe *keyframe, + char *buf, int buf_size); + +/** + * @brief Set a string value directly (live). + */ +int oaknode_keyframe_set_value_string(OakNodeKeyframe *keyframe, + const char *value); + +/** + * @brief Create a set-string-value command. + */ +int oaknode_keyframe_set_value_string_undoable(OakNodeKeyframe *keyframe, + const char *value, + OakUndoCommand **out_command); + +/** + * @brief The keyframe's interpolation type (oaknode_keyframe_type). + */ +int oaknode_keyframe_get_type(const OakNodeKeyframe *keyframe, int *out_type); + +/** + * @brief Set the interpolation type directly (live, + * NodeKeyframe::set_type(), which adjusts neighbouring bezier handles). + */ +int oaknode_keyframe_set_type(OakNodeKeyframe *keyframe, int type); + +/** + * @brief Create a set-type command (same semantics as the live variant). + */ +int oaknode_keyframe_set_type_undoable(OakNodeKeyframe *keyframe, int type, + OakUndoCommand **out_command); + +/** + * @brief A bezier control point (`handle` is an + * oaknode_keyframe_bezier). + */ +int oaknode_keyframe_get_bezier_control(const OakNodeKeyframe *keyframe, + int handle, double *out_x, + double *out_y); + +/** + * @brief Set a bezier control point directly (live). + */ +int oaknode_keyframe_set_bezier_control(OakNodeKeyframe *keyframe, int handle, + double x, double y); + +/** + * @brief Create a set-bezier-control command. + */ +int oaknode_keyframe_set_bezier_control_undoable(OakNodeKeyframe *keyframe, + int handle, double x, double y, + OakUndoCommand **out_command); + +/** + * @brief The keyframe's track index. + */ +int oaknode_keyframe_get_track(const OakNodeKeyframe *keyframe, + int *out_track); + +/** + * @brief The keyframe's element index. + */ +int oaknode_keyframe_get_element(const OakNodeKeyframe *keyframe, + int *out_element); + +/** + * @brief The id of the input this keyframe belongs to. Two-stage getter. + */ +int oaknode_keyframe_get_input(const OakNodeKeyframe *keyframe, char *buf, + int buf_size); + +/** + * @brief The node this keyframe belongs to (borrowed handle), or NULL + * when orphaned. OAKNODE_OK either way. + */ +int oaknode_keyframe_get_parent(const OakNodeKeyframe *keyframe, + OakNodeNode **out_node); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_KEYFRAME_H diff --git a/include/node/node.h b/include/node/node.h new file mode 100644 index 000000000..232516a5a --- /dev/null +++ b/include/node/node.h @@ -0,0 +1,487 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_NODE_H +#define OAK_EDITOR_NODE_NODE_H + +#include + +#include "node/error.h" +#include "undo/undocommand.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file node.h + * @brief C ABI for olive::Node (src/node/src/node.h). + * + * The handle IS the C++ object pointer (M3 handle convention 3): an + * OakNodeNode is a reinterpreted olive::Node, no wrapper allocation. + * Handles borrowed from a graph become invalid when the owning project or + * node is destroyed. Owned handles (from oaknode_factory_create_from_id() + * or oaknode_node_create_copy()) must be released with + * oaknode_node_free() while still orphaned; once a node lives in a project + * graph its lifetime belongs to the graph. + * + * Parameter values cross the boundary as the POD oaknode_value; the + * meaningful fields depend on its type (oaknode_value_type). String-typed + * inputs (NodeValue::k_file/k_text/k_font/k_str_combo) do not fit the POD + * and use the dedicated *_input_string() pair (two-stage buf/size getters + * return the required size including the terminating NUL). + * + * Every mutating function comes in a live variant (applies immediately) + * and an undoable variant (suffix _undoable) that creates an + * olive::UndoCommand without executing it and returns it as an owned + * OakUndoCommand handle. Execute it with oakundo_command_redo_now(), + * push it onto an OakUndoStack, or release it with + * oakundo_command_free(). + */ + +/** + * @brief Value type of an oaknode_value / a node input. + * + * Pinned mapping to olive::NodeValue::Type (src/node/src/value.h): + * NONE -> k_none, INT -> k_int, FLOAT -> k_float, BOOL -> k_boolean, + * RATIONAL -> k_rational, COLOR -> k_color, VEC2 -> k_vec2, + * VEC3 -> k_vec3, VEC4 -> k_vec4, COMBO -> k_combo, + * STRING -> k_file (string-family inputs: k_file/k_text/k_font/ + * k_str_combo, handled by the dedicated string functions). Types without + * a POD representation (texture, samples, matrix, params, bezier, binary, + * ...) report as OAKNODE_VALUE_NONE. + */ +typedef enum oaknode_value_type { + OAKNODE_VALUE_NONE = 0, + OAKNODE_VALUE_INT, /**< num (olive k_int, int64_t) */ + OAKNODE_VALUE_FLOAT, /**< f[0] (olive k_float, double) */ + OAKNODE_VALUE_BOOL, /**< num 0/1 (olive k_boolean) */ + OAKNODE_VALUE_RATIONAL, /**< num/den (olive k_rational) */ + OAKNODE_VALUE_COLOR, /**< f[0..3] = r,g,b,a (olive k_color) */ + OAKNODE_VALUE_VEC2, /**< f[0..1] (olive k_vec2) */ + OAKNODE_VALUE_VEC3, /**< f[0..2] (olive k_vec3) */ + OAKNODE_VALUE_VEC4, /**< f[0..3] (olive k_vec4) */ + OAKNODE_VALUE_COMBO, /**< num = selected index (olive k_combo) */ + OAKNODE_VALUE_STRING, /**< k_file string family; string APIs only */ + OAKNODE_VALUE_COUNT +} oaknode_value_type; + +/** + * @brief POD parameter value. Only the fields documented for the value's + * `type` are meaningful. + */ +typedef struct oaknode_value { + int type; /**< oaknode_value_type. */ + int64_t num; /**< INT/COMBO value, BOOL 0/1, RATIONAL numerator. */ + int64_t den; /**< RATIONAL denominator. */ + double f[4]; /**< FLOAT f[0]; VEC2/3/4 f[0..n-1]; COLOR r,g,b,a. */ +} oaknode_value; + +/** + * @brief Opaque node handle (olive::Node). + */ +typedef struct OakNodeNode OakNodeNode; + +/** + * @brief Number of live owned objects created through this API + * (nodes from oaknode_factory_create_from_id()/oaknode_node_create_copy(), + * keyframes, groups, traversers, traverser databases). Debug aid for + * leak checking; thread-unsafe, test/diagnostic use only. + */ +int oaknode_debug_alive_count(void); + +/* ---- Metadata --------------------------------------------------------- */ + +/** + * @brief The node's unique type id (Node::id(), e.g. + * "org.olivevideoeditor.Olive.solidgenerator"). Two-stage getter. + * + * @return Required buffer size in bytes including the terminating NUL + * (non-negative), or a negative OAKNODE_E_* error code. + */ +int oaknode_node_get_id(const OakNodeNode *node, char *buf, int buf_size); + +/** + * @brief The node's display name (Node::name()). Two-stage getter, + * same return convention as oaknode_node_get_id(). + */ +int oaknode_node_get_name(const OakNodeNode *node, char *buf, int buf_size); + +/** + * @brief The node's user label (Node::get_label()). Two-stage getter, + * same return convention as oaknode_node_get_id(). + */ +int oaknode_node_get_label(const OakNodeNode *node, char *buf, int buf_size); + +/** + * @brief Set the node's user label directly (Node::set_label(), live). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_label(OakNodeNode *node, const char *label); + +/** + * @brief Create a label-change command (olive::NodeRenameCommand). + * + * The command is NOT executed; `out_command` receives an owned command + * handle. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_label_undoable(OakNodeNode *node, const char *label, + OakUndoCommand **out_command); + +/** + * @brief The node's override color index (Node::get_override_color(); + * -1 = none). + * + * @param out_value Receives the result. Must not be NULL. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_get_override_color(const OakNodeNode *node, int *out_value); + +/** + * @brief Set the override color index directly (-1 = none; live). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_override_color(OakNodeNode *node, int index); + +/** + * @brief Create an override-color command (olive::NodeOverrideColorCommand). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_override_color_undoable(OakNodeNode *node, int index, + OakUndoCommand **out_command); + +/** + * @brief 1 if the node is enabled (the boolean "enabled_in" input's + * standard value). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_is_enabled(const OakNodeNode *node, int *out_value); + +/** + * @brief Set the node's enabled state directly (live). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_enabled(OakNodeNode *node, int enabled); + +/** + * @brief Create an enabled-state command + * (olive::NodeParamSetStandardValueCommand on "enabled_in"). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_set_enabled_undoable(OakNodeNode *node, int enabled, + OakUndoCommand **out_command); + +/* ---- Input introspection ------------------------------------------------ */ + +/** + * @brief Number of declared inputs (Node::inputs(); array elements are not + * counted separately). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_node_input_count(const OakNodeNode *node, int *out_count); + +/** + * @brief The input id at `index` (Node::inputs()). Two-stage getter; + * returns OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_node_input_id(const OakNodeNode *node, int index, char *buf, + int buf_size); + +/** + * @brief The input's value type mapped to oaknode_value_type (see the + * pinned mapping on oaknode_value_type). OAKNODE_E_NOT_FOUND for an + * unknown input id. + */ +int oaknode_node_input_get_type(const OakNodeNode *node, const char *input_id, + int *out_type); + +/** + * @brief 1 if the input currently has a connected edge + * (Node::is_input_connected()). OAKNODE_E_NOT_FOUND for an unknown id. + */ +int oaknode_node_input_is_connected(const OakNodeNode *node, + const char *input_id, int *out_value); + +/** + * @brief 1 if the input accepts connections (Node::is_input_connectable()). + * OAKNODE_E_NOT_FOUND for an unknown id. + */ +int oaknode_node_input_is_connectable(const OakNodeNode *node, + const char *input_id, int *out_value); + +/** + * @brief The human-readable name of the input (Node::get_input_name()). + * Two-stage getter; OAKNODE_E_NOT_FOUND for an unknown id. + */ +int oaknode_node_get_input_name(const OakNodeNode *node, const char *input_id, + char *buf, int buf_size); + +/** + * @brief The node feeding this input, or NULL when not connected + * (Node::get_connected_output(), element -1). `out_node` receives a + * borrowed handle. OAKNODE_E_NOT_FOUND for an unknown input id. + */ +int oaknode_node_input_get_connected_node(const OakNodeNode *node, + const char *input_id, + OakNodeNode **out_node); + +/* ---- Parameter access ----------------------------------------------------- */ + +/** + * @brief Read an input's standard value (Node::get_standard_value()) + * mapped into `out`. + * + * String-family inputs fail with OAKNODE_E_INVALID (use + * oaknode_node_get_input_string()); types without a POD representation + * fail with OAKNODE_E_FAILED; an unknown input id fails with + * OAKNODE_E_NOT_FOUND. + */ +int oaknode_node_get_input(const OakNodeNode *node, const char *input_id, + oaknode_value *out); + +/** + * @brief Write an input's standard value directly (live, + * Node::set_standard_value()). + * + * `v->type` must match the input's declared type; OAKNODE_VALUE_STRING is + * rejected (use oaknode_node_set_input_string()). + */ +int oaknode_node_set_input(OakNodeNode *node, const char *input_id, + const oaknode_value *v); + +/** + * @brief Create a set-standard-value command + * (olive::NodeParamSetStandardValueCommand, track -1 semantics via the + * whole-value reference on track 0). + * + * Same type rules as oaknode_node_set_input(). + */ +int oaknode_node_set_input_undoable(OakNodeNode *node, const char *input_id, + const oaknode_value *v, + OakUndoCommand **out_command); + +/** + * @brief Read a string-family input's standard value. Two-stage getter. + */ +int oaknode_node_get_input_string(const OakNodeNode *node, + const char *input_id, char *buf, + int buf_size); + +/** + * @brief Write a string-family input's standard value directly (live). + */ +int oaknode_node_set_input_string(OakNodeNode *node, const char *input_id, + const char *value); + +/** + * @brief Create a set-standard-value command for a string-family input. + */ +int oaknode_node_set_input_string_undoable(OakNodeNode *node, + const char *input_id, + const char *value, + OakUndoCommand **out_command); + +/* ---- Graph editing -------------------------------------------------------- */ + +/** + * @brief Connect `output_node`'s output into `input_node`'s `input_id` + * directly (live, Node::connect_edge(), element -1). + * + * Fails with OAKNODE_E_NOT_FOUND for an unknown input id, + * OAKNODE_E_INVALID when the input is not connectable, and + * OAKNODE_E_STATE when the input is already connected or the nodes belong + * to different graphs. + */ +int oaknode_node_connect(OakNodeNode *output_node, OakNodeNode *input_node, + const char *input_id); + +/** + * @brief Create an edge-add command (olive::NodeEdgeAddCommand, + * element -1). Same validation as oaknode_node_connect() except the + * different-graph check (the command may legitimately be redone after + * graph changes). + */ +int oaknode_node_connect_undoable(OakNodeNode *output_node, + OakNodeNode *input_node, + const char *input_id, + OakUndoCommand **out_command); + +/** + * @brief Remove the edge feeding `input_node`'s `input_id` directly + * (live, Node::disconnect_edge(), element -1). OAKNODE_E_NOT_FOUND when + * the input is unknown or not connected. + */ +int oaknode_node_disconnect(OakNodeNode *input_node, const char *input_id); + +/** + * @brief Create an edge-remove command (olive::NodeEdgeRemoveCommand, + * element -1). OAKNODE_E_NOT_FOUND when not connected. + */ +int oaknode_node_disconnect_undoable(OakNodeNode *input_node, + const char *input_id, + OakUndoCommand **out_command); + +/** + * @brief Number of outgoing edges (Node::output_connections()). + */ +int oaknode_node_output_connection_count(const OakNodeNode *node, + int *out_count); + +/** + * @brief The node at the input end of outgoing edge `index` + * (borrowed handle). OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_node_output_connection_node_at(const OakNodeNode *node, int index, + OakNodeNode **out_node); + +/** + * @brief The input id at the input end of outgoing edge `index`. + * Two-stage getter; OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_node_output_connection_input_id_at(const OakNodeNode *node, + int index, char *buf, + int buf_size); + +/** + * @brief The input element at the input end of outgoing edge `index` + * (-1 for non-array inputs). OAKNODE_E_NOT_FOUND for an out-of-range + * index. + */ +int oaknode_node_output_connection_element_at(const OakNodeNode *node, + int index, int *out_element); + +/* ---- Links --------------------------------------------------------------- */ + +/** + * @brief Link two nodes directly (live, Node::link()). `out_linked` + * receives 1 on success, 0 when the link was rejected (e.g. either node + * rejects links). `out_linked` may be NULL. + */ +int oaknode_node_link(OakNodeNode *a, OakNodeNode *b, int *out_linked); + +/** + * @brief Unlink two nodes directly (live, Node::unlink()). + * `out_unlinked` receives 1 on success, 0 otherwise; may be NULL. + */ +int oaknode_node_unlink(OakNodeNode *a, OakNodeNode *b, int *out_unlinked); + +/** + * @brief Create a link/unlink command (olive::NodeLinkCommand; + * `link` != 0 links, 0 unlinks). + */ +int oaknode_node_link_undoable(OakNodeNode *a, OakNodeNode *b, int link, + OakUndoCommand **out_command); + +/** + * @brief 1 if the two nodes are linked (Node::are_linked()). + */ +int oaknode_node_are_linked(const OakNodeNode *a, const OakNodeNode *b, + int *out_value); + +/** + * @brief Number of linked nodes (Node::links()). + */ +int oaknode_node_link_count(const OakNodeNode *node, int *out_count); + +/** + * @brief The linked node at `index` (borrowed handle). + * OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_node_link_at(const OakNodeNode *node, int index, + OakNodeNode **out_node); + +/* ---- Context positions ---------------------------------------------------- */ + +/** + * @brief Number of context entries (Node::get_context_positions()). + */ +int oaknode_node_context_count(const OakNodeNode *node, int *out_count); + +/** + * @brief The context node at `index` (borrowed handle). + * OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_node_context_node_at(const OakNodeNode *node, int index, + OakNodeNode **out_node); + +/** + * @brief The node's position in `context` (any out pointer may be NULL). + * OAKNODE_E_NOT_FOUND when the context does not contain this node. + */ +int oaknode_node_get_context_position(const OakNodeNode *node, + OakNodeNode *context, double *out_x, + double *out_y, int *out_expanded); + +/** + * @brief Set the node's position in `context` directly (live, + * Node::set_node_position_in_context() + set_node_expanded_in_context()). + */ +int oaknode_node_set_context_position(OakNodeNode *node, OakNodeNode *context, + double x, double y, int expanded); + +/** + * @brief Create a set-position command (olive::NodeSetPositionCommand). + */ +int oaknode_node_set_context_position_undoable(OakNodeNode *node, + OakNodeNode *context, double x, + double y, int expanded, + OakUndoCommand **out_command); + +/** + * @brief Remove the node from `context` directly (live). + * OAKNODE_E_NOT_FOUND when not contained. + */ +int oaknode_node_remove_from_context(OakNodeNode *node, OakNodeNode *context); + +/* ---- Lifetime --------------------------------------------------------------- */ + +/** + * @brief Create a standalone copy of the node (Node::copy()). The copy is + * NOT added to any graph; the caller owns it and must release it with + * oaknode_node_free() while it is still orphaned. Returns NULL for NULL. + */ +OakNodeNode *oaknode_node_create_copy(const OakNodeNode *node); + +/** + * @brief Destroy an OWNED node immediately (C++ delete). NULL is a no-op. + * + * ONLY valid for owned handles that were never added to a graph: the + * products of oaknode_factory_create_from_id(), + * oaknode_node_create_copy() and oaknode_group_create() while still + * orphaned. Freeing a graph-owned node, or freeing twice, is a + * use-after-free. + */ +void oaknode_node_free(OakNodeNode *node); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_NODE_H diff --git a/include/node/project.h b/include/node/project.h new file mode 100644 index 000000000..ff82813a6 --- /dev/null +++ b/include/node/project.h @@ -0,0 +1,237 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_PROJECT_H +#define OAK_EDITOR_NODE_PROJECT_H + +#include "node/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file project.h + * @brief C ABI for olive::Project (oaknode) + * + * An OakNodeProject owns its whole node graph: nodes added with + * oaknode_project_add_node() (directly, or indirectly through the folder and + * footage families) are deleted by oaknode_project_free(). Handles to nodes, + * folders and footage obtained from a project are borrowed views and must not + * be freed. + * + * Conventions (shared by all oaknode C API families): + * - Return codes: 0 (OAKNODE_OK) on success, a negative OAKNODE_E_* code on + * failure. + * - String getters are two-stage: pass buf == NULL (or a short buffer) to + * query the required size; the return value is the required buffer size in + * bytes INCLUDING the terminating NUL. The output is NUL-terminated + * whenever buf_size > 0. + * - NULL handles yield OAKNODE_E_INVALID (or a no-op for free()). + * - Disk save/load of project files is NOT part of this layer; it belongs to + * oakstorage (milestone M10). + */ + +/** + * @brief Opaque project handle. Owned by the caller; release with + * oaknode_project_free(). + */ +typedef struct OakNodeProject OakNodeProject; + +/** + * @brief Opaque node handle (defined by the node family; forward-declared + * here so the headers can be included in any order). + */ +typedef struct OakNodeNode OakNodeNode; + +/** + * @brief Opaque folder handle (defined in node/folder.h; forward-declared + * here so the headers can be included in any order). Borrowed from the + * owning project. + */ +typedef struct OakNodeFolder OakNodeFolder; + +/** + * @brief Create an empty project shell. + * + * The project has no root folder until oaknode_project_initialize() is + * called (mirrors Project::initialize()). + * + * @return Project handle, or NULL on allocation failure. + */ +OakNodeProject *oaknode_project_init(void); + +/** + * @brief Destroy a project and every node it owns. NULL is a no-op. + */ +void oaknode_project_free(OakNodeProject *project); + +/** + * @brief Initialize the project: create the root folder (Project::initialize()). + * + * @return OAKNODE_OK, or OAKNODE_E_STATE if already initialized. + */ +int oaknode_project_initialize(OakNodeProject *project); + +/** + * @brief Destructively destroy all nodes in the graph (Project::clear()). + * + * The project shell stays usable; oaknode_project_initialize() may be called + * again afterwards. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_clear(OakNodeProject *project); + +/** + * @brief Borrowed handle of the project's root folder (Project::root()). + * + * NULL if the project has not been initialized. + */ +OakNodeFolder *oaknode_project_root(OakNodeProject *project); + +/** + * @brief Project display name (Project::name(): the filename's base name, or + * "(untitled)"). Two-stage string getter. + * + * @return Required buffer size in bytes including the NUL, or a negative + * OAKNODE_E_* error code. + */ +int oaknode_project_name(const OakNodeProject *project, char *buf, int buf_size); + +/** + * @brief Full path the project was saved as, or "" if untitled + * (Project::filename()). Two-stage string getter. + */ +int oaknode_project_filename(const OakNodeProject *project, char *buf, + int buf_size); + +/** + * @brief Display name safe for window titles (Project::pretty_filename()). + * Two-stage string getter. + */ +int oaknode_project_pretty_filename(const OakNodeProject *project, char *buf, + int buf_size); + +/** + * @brief Set the project's filename (Project::set_filename()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_set_filename(OakNodeProject *project, const char *filename); + +/** + * @brief 1 if the project has unsaved changes, 0 otherwise + * (Project::is_modified()). Negative OAKNODE_E_* code on NULL. + */ +int oaknode_project_is_modified(const OakNodeProject *project); + +/** + * @brief Set the modified flag (Project::set_modified()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_set_modified(OakNodeProject *project, int modified); + +/** + * @brief 1 if the project is new (untitled and unmodified, Project::is_new()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_project_is_new(const OakNodeProject *project); + +/** + * @brief Effective cache directory (Project::cache_path(), honoring the cache + * location setting). Two-stage string getter. + */ +int oaknode_project_cache_path(const OakNodeProject *project, char *buf, + int buf_size); + +/** + * @brief Cache location setting enum value + * (Project::get_cache_location_setting(): 0 = default location, + * 1 = alongside project, 2 = custom path). Negative OAKNODE_E_* code on NULL. + */ +int oaknode_project_get_cache_location_setting(const OakNodeProject *project); + +/** + * @brief Set the cache location setting (0/1/2, see + * oaknode_project_get_cache_location_setting()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_set_cache_location_setting(OakNodeProject *project, + int setting); + +/** + * @brief Custom cache directory, or "" when none is set + * (Project::get_custom_cache_path()). Two-stage string getter. + */ +int oaknode_project_get_custom_cache_path(const OakNodeProject *project, + char *buf, int buf_size); + +/** + * @brief Set a custom cache directory (Project::set_custom_cache_path()). + * NULL clears it. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_set_custom_cache_path(OakNodeProject *project, + const char *path); + +/** + * @brief Project UUID string (Project::get_uuid()). Two-stage string getter. + */ +int oaknode_project_get_uuid(const OakNodeProject *project, char *buf, + int buf_size); + +/** + * @brief Add a node to the graph; the project takes ownership + * (Project::add_node()). + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_project_add_node(OakNodeProject *project, OakNodeNode *node); + +/** + * @brief Detach a node from the graph without deleting it + * (Project::remove_node()). + * + * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND if the node is not in the graph, or + * another negative OAKNODE_E_* error code. + */ +int oaknode_project_remove_node(OakNodeProject *project, OakNodeNode *node); + +/** + * @brief Number of nodes belonging to the graph (Project::nodes().size()). + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_project_node_count(const OakNodeProject *project); + +/** + * @brief Borrowed handle of the graph node at `index`, or NULL when out of + * range. + */ +OakNodeNode *oaknode_project_node_at(const OakNodeProject *project, int index); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_PROJECT_H diff --git a/include/node/sequence.h b/include/node/sequence.h new file mode 100644 index 000000000..512fe6e6c --- /dev/null +++ b/include/node/sequence.h @@ -0,0 +1,163 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_SEQUENCE_H +#define OAK_EDITOR_NODE_SEQUENCE_H + +#ifndef __cplusplus +#include +#endif + +#include "common/videoparams.h" +#include "node/error.h" +#include "olive/core/oakcore/audioparams.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a sequence (olive::Sequence). + * + * The handle IS the C++ object pointer; no wrapper is allocated. + */ +typedef struct OakNodeSequence OakNodeSequence; + +/** + * @brief Opaque handle to a track list (olive::TrackList), see node/track.h. + */ +typedef struct OakNodeTrackList OakNodeTrackList; + +/** + * @brief Opaque handle to a track (olive::Track), see node/track.h. + */ +typedef struct OakNodeTrack OakNodeTrack; + +/** + * @brief Create an empty sequence with zero tracks. + * + * @return Sequence handle, or NULL on allocation failure. + */ +OakNodeSequence *oaknode_sequence_create(void); + +/** + * @brief Destroy a sequence and its track lists. No-op on NULL. + * + * Tracks and blocks connected to the sequence are owned by the graph and + * are not deleted here; the caller must have torn them down first. + */ +void oaknode_sequence_free(OakNodeSequence *sequence); + +/** + * @brief Borrowed handle to the per-type track list. + * + * @param type One of OAKNODE_TRACK_TYPE_VIDEO / _AUDIO / _SUBTITLE. + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND (bad type). + */ +int oaknode_sequence_get_track_list(OakNodeSequence *sequence, int type, + OakNodeTrackList **out); + +/** + * @brief Number of connected tracks of the given type. + */ +int oaknode_sequence_get_track_count(OakNodeSequence *sequence, int type, + int *count); + +/** + * @brief Borrowed handle to the track of `type` at `index`. + */ +int oaknode_sequence_get_track_at(OakNodeSequence *sequence, int type, + int index, OakNodeTrack **out); + +/** + * @brief Flat track cache across all types (olive::Sequence::get_tracks()). + */ +int oaknode_sequence_get_all_track_count(OakNodeSequence *sequence, int *count); +int oaknode_sequence_get_all_track_at(OakNodeSequence *sequence, int index, + OakNodeTrack **out); + +/** + * @brief Playhead position in sequence time. + */ +int oaknode_sequence_get_playhead(OakNodeSequence *sequence, int *numerator, + int *denominator); +int oaknode_sequence_set_playhead(OakNodeSequence *sequence, int numerator, + int denominator); + +/** + * @brief Cached overall/video/audio lengths (olive::ViewerOutput). + */ +int oaknode_sequence_get_length(OakNodeSequence *sequence, int *numerator, + int *denominator); +int oaknode_sequence_get_video_length(OakNodeSequence *sequence, + int *numerator, int *denominator); +int oaknode_sequence_get_audio_length(OakNodeSequence *sequence, + int *numerator, int *denominator); + +/** + * @brief Recompute the cached lengths from the track lists + * (olive::ViewerOutput::verify_length()). + */ +int oaknode_sequence_verify_length(OakNodeSequence *sequence); + +/* --------------------------------------------------- Video/audio params */ + +/** + * @brief Number of video/audio parameter slots. + */ +int oaknode_sequence_get_video_stream_count(OakNodeSequence *sequence, + int *count); +int oaknode_sequence_get_audio_stream_count(OakNodeSequence *sequence, + int *count); + +/** + * @brief Video parameters at `index` as a NEW handle owned by the caller + * (release with oakcommon_videoparams_free()). + * + * @return OAKNODE_OK, OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND or + * OAKNODE_E_NOMEM. + */ +int oaknode_sequence_get_video_params(OakNodeSequence *sequence, int index, + OakCommonVideoParams **out); + +/** + * @brief Replace the video parameters at `index` with a copy of `params`. + */ +int oaknode_sequence_set_video_params(OakNodeSequence *sequence, int index, + const OakCommonVideoParams *params); + +/** + * @brief Audio parameters at `index` as a NEW handle owned by the caller + * (release with oakcore_audioparams_free()). + */ +int oaknode_sequence_get_audio_params(OakNodeSequence *sequence, int index, + OakAudioParams **out); + +/** + * @brief Replace the audio parameters at `index` with a copy of `params`. + */ +int oaknode_sequence_set_audio_params(OakNodeSequence *sequence, int index, + const OakAudioParams *params); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_SEQUENCE_H diff --git a/include/node/serializer.h b/include/node/serializer.h new file mode 100644 index 000000000..f64cbff2f --- /dev/null +++ b/include/node/serializer.h @@ -0,0 +1,244 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_SERIALIZER_H +#define OAK_EDITOR_NODE_SERIALIZER_H + +#include "node/error.h" +#include "node/project.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file serializer.h + * @brief C ABI for olive::ProjectSerializer (oaknode), in-memory form + * + * Clipboard copy/paste and node-graph XML round trips without touching the + * filesystem: "copy" is oaknode_serializer_save_to_xml() (serialize a + * SaveData to an XML string), "paste" is oaknode_serializer_load_from_xml() + * (parse an XML string into a project, exposing the resulting LoadData). + * System-clipboard integration and on-disk .ove save/load live in the + * facade / oakstorage layers (M9/M10), not here. + * + * oaknode_serializer_initialize() must be called before any save/load; it + * registers the versioned serializers and the node factory the loaders use + * to instantiate nodes by id. + */ + +/** @brief Load type: a whole project. */ +#define OAKNODE_SERIALIZER_LOAD_PROJECT 0 +/** @brief Load type: only nodes (clipboard node-graph paste). */ +#define OAKNODE_SERIALIZER_LOAD_ONLY_NODES 1 +/** @brief Load type: only clips (timeline family). */ +#define OAKNODE_SERIALIZER_LOAD_ONLY_CLIPS 2 +/** @brief Load type: only markers (timeline family). */ +#define OAKNODE_SERIALIZER_LOAD_ONLY_MARKERS 3 +/** @brief Load type: only keyframes (keyframe family). */ +#define OAKNODE_SERIALIZER_LOAD_ONLY_KEYFRAMES 4 + +/** @brief Serializer result code: success. */ +#define OAKNODE_SERIALIZER_OK 0 +/** @brief Serializer result code: data written by a too-old format. */ +#define OAKNODE_SERIALIZER_TOO_OLD 1 +/** @brief Serializer result code: data written by a too-new format. */ +#define OAKNODE_SERIALIZER_TOO_NEW 2 +/** @brief Serializer result code: unrecognizable format version. */ +#define OAKNODE_SERIALIZER_UNKNOWN_VERSION 3 +/** @brief Serializer result code: file I/O error (unused in-memory). */ +#define OAKNODE_SERIALIZER_FILE_ERROR 4 +/** @brief Serializer result code: XML parse error. */ +#define OAKNODE_SERIALIZER_XML_ERROR 5 +/** @brief Serializer result code: overwrite error (unused in-memory). */ +#define OAKNODE_SERIALIZER_OVERWRITE_ERROR 6 +/** @brief Serializer result code: no data to load. */ +#define OAKNODE_SERIALIZER_NO_DATA 7 + +/** + * @brief Opaque save descriptor (wraps ProjectSerializer::SaveData). + * Owned by the caller; release with oaknode_serializer_savedata_free(). + */ +typedef struct OakNodeSerializerSaveData OakNodeSerializerSaveData; + +/** + * @brief Opaque load result (wraps ProjectSerializer::LoadData). + * Owned by the caller; release with oaknode_serializer_loaddata_free(). + * Node handles obtained from it are borrowed from the target project. + */ +typedef struct OakNodeSerializerLoadData OakNodeSerializerLoadData; + +/** + * @brief Register the versioned serializers and initialize the node factory. + * Idempotent. Must be called before any save/load. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_serializer_initialize(void); + +/** + * @brief Tear down the serializers and the node factory registered by + * oaknode_serializer_initialize(). Safe to call when not initialized. + */ +void oaknode_serializer_shutdown(void); + +/** + * @brief Create a save descriptor. + * + * @param load_type One of OAKNODE_SERIALIZER_LOAD_*; use + * OAKNODE_SERIALIZER_LOAD_ONLY_NODES for clipboard-style node copies. + * @param project Context project (borrowed), may be NULL for load types + * that do not require it. + * + * @return Save-data handle, or NULL on failure. + */ +OakNodeSerializerSaveData *oaknode_serializer_savedata_create( + int load_type, OakNodeProject *project); + +/** + * @brief Destroy a save descriptor. NULL is a no-op. + */ +void oaknode_serializer_savedata_free(OakNodeSerializerSaveData *save_data); + +/** + * @brief Restrict serialization to the given nodes + * (SaveData::set_only_serialize_nodes()). `nodes` is an array of `count` + * borrowed node handles. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_serializer_savedata_set_nodes( + OakNodeSerializerSaveData *save_data, OakNodeNode *const *nodes, int count); + +/** + * @brief Attach a free-form (key, value) property to a node in the + * serialized output (SaveData::set_properties()); used for graph positions + * and clip metadata. Replaces the value if the (node, key) pair exists. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_serializer_savedata_set_property( + OakNodeSerializerSaveData *save_data, OakNodeNode *node, const char *key, + const char *value); + +/** + * @brief Serialize to an in-memory XML document ("copy"). Two-stage string + * getter: pass buf == NULL to query the size. + * + * @return Required buffer size in bytes including the NUL, or a negative + * OAKNODE_E_* error code (OAKNODE_E_STATE if the serializers have + * not been initialized). + */ +int oaknode_serializer_save_to_xml(OakNodeSerializerSaveData *save_data, + char *buf, int buf_size); + +/** + * @brief Parse an in-memory XML document into `project` ("paste"). + * + * @param project Target project (borrowed), may be NULL for load types that + * do not attach nodes to a project. + * @param xml Complete XML document text. Must not be NULL. + * @param load_type One of OAKNODE_SERIALIZER_LOAD_*. + * @param out_result Receives one of the OAKNODE_SERIALIZER_* result codes. + * Must not be NULL. + * @param out_load_data Receives the load result on OAKNODE_SERIALIZER_OK + * (caller-owned, may be NULL if the caller does not need it; + * receives NULL on failure). + * @param details_buf Optional human-readable error detail buffer + * (two-stage convention is NOT used; truncation is silent). May be + * NULL. + * @param details_buf_size Size of details_buf. + * + * @return OAKNODE_OK if the call itself succeeded (inspect *out_result for + * the serializer outcome), or a negative OAKNODE_E_* error code. + */ +int oaknode_serializer_load_from_xml(OakNodeProject *project, const char *xml, + int load_type, int *out_result, + OakNodeSerializerLoadData **out_load_data, + char *details_buf, int details_buf_size); + +/** + * @brief Destroy a load result. NULL is a no-op. + * + * Does not delete the loaded nodes: they are newly created objects owned by + * the CALLER until adopted into a project with oaknode_project_add_node() + * (or attached under a folder); otherwise they leak. + */ +void oaknode_serializer_loaddata_free(OakNodeSerializerLoadData *load_data); + +/** + * @brief Number of nodes created by the load. Negative OAKNODE_E_* code on + * NULL. + */ +int oaknode_serializer_loaddata_node_count( + const OakNodeSerializerLoadData *load_data); + +/** + * @brief Borrowed handle of the loaded node at `index`, or NULL when out of + * range. + */ +OakNodeNode *oaknode_serializer_loaddata_node_at( + const OakNodeSerializerLoadData *load_data, int index); + +/** + * @brief Look up a serialized property attached to a loaded node. + * Two-stage string getter. + * + * @return Required buffer size in bytes including the NUL, + * OAKNODE_E_NOT_FOUND if the (node, key) pair is absent, or another + * negative OAKNODE_E_* error code. + */ +int oaknode_serializer_loaddata_get_property( + const OakNodeSerializerLoadData *load_data, OakNodeNode *node, + const char *key, char *buf, int buf_size); + +/** + * @brief Number of promised (deferred) connections in the load result. + * Negative OAKNODE_E_* code on NULL. + */ +int oaknode_serializer_loaddata_connection_count( + const OakNodeSerializerLoadData *load_data); + +/** + * @brief Read the promised connection at `index`. + * + * All output parameters except the input-id buffer are required; + * `input_id_buf` follows the two-stage string convention inside a + * fixed call: pass NULL/0 to skip copying the id. + * + * @param out_output_node Receives the output (source) node. + * @param out_input_node Receives the input (destination) node. + * @param input_id_buf Receives the input id string, may be NULL. + * @param input_id_buf_size Size of input_id_buf. + * @param out_element Receives the input element index. + * + * @return OAKNODE_OK, OAKNODE_E_NOT_FOUND when out of range, or another + * negative OAKNODE_E_* error code. + */ +int oaknode_serializer_loaddata_connection_at( + const OakNodeSerializerLoadData *load_data, int index, + OakNodeNode **out_output_node, OakNodeNode **out_input_node, + char *input_id_buf, int input_id_buf_size, int *out_element); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_SERIALIZER_H diff --git a/include/node/track.h b/include/node/track.h new file mode 100644 index 000000000..856183d0a --- /dev/null +++ b/include/node/track.h @@ -0,0 +1,279 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_TRACK_H +#define OAK_EDITOR_NODE_TRACK_H + +#ifndef __cplusplus +#include +#endif + +#include "node/error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a track (olive::Track). + * + * The handle IS the C++ object pointer; no wrapper is allocated. + */ +typedef struct OakNodeTrack OakNodeTrack; + +/** + * @brief Opaque handle to a per-type track container (olive::TrackList). + * + * Borrowed from oaknode_sequence_get_track_list(); invalidated when the + * owning sequence is destroyed. + */ +typedef struct OakNodeTrackList OakNodeTrackList; + +/** + * @brief Opaque handle to a block (olive::Block), see node/block.h. + */ +typedef struct OakNodeBlock OakNodeBlock; + +/** + * @brief Opaque handle to a sequence (olive::Sequence), see node/sequence.h. + */ +typedef struct OakNodeSequence OakNodeSequence; + +/** + * @brief Track types, matching olive::Track::Type. + */ +enum OakNodeTrackType { + OAKNODE_TRACK_TYPE_NONE = -1, + OAKNODE_TRACK_TYPE_VIDEO = 0, + OAKNODE_TRACK_TYPE_AUDIO = 1, + OAKNODE_TRACK_TYPE_SUBTITLE = 2, + OAKNODE_TRACK_TYPE_COUNT = 3 +}; + +/* ---------------------------------------------------------------- Track */ + +/** + * @brief Create a track of the given type (OakNodeTrackType value). + * + * The caller owns the track until it is added to a track list; a track + * that was never added must be released with oaknode_track_free(). + * + * @return Track handle, or NULL on invalid type / allocation failure. + */ +OakNodeTrack *oaknode_track_create(int type); + +/** + * @brief Destroy a track. No-op on NULL. + * + * The track must have been removed from its track list first. + */ +void oaknode_track_free(OakNodeTrack *track); + +/** + * @brief Track type (OakNodeTrackType values). + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_track_get_type(OakNodeTrack *track, int *type); +int oaknode_track_set_type(OakNodeTrack *track, int type); + +/** + * @brief Track height in internal units (olive::Track::get/set_track_height). + */ +int oaknode_track_get_height(OakNodeTrack *track, double *height); +int oaknode_track_set_height(OakNodeTrack *track, double height); + +/** + * @brief Track height in pixels (converted through the default font height). + */ +int oaknode_track_get_height_in_pixels(OakNodeTrack *track, int *height); +int oaknode_track_set_height_in_pixels(OakNodeTrack *track, int height); + +/** + * @brief Default / minimum track heights in pixels (static). + */ +int oaknode_track_get_default_height_in_pixels(void); +int oaknode_track_get_minimum_height_in_pixels(void); + +/** + * @brief Index of the track inside its track list. + */ +int oaknode_track_get_index(OakNodeTrack *track, int *index); +int oaknode_track_set_index(OakNodeTrack *track, int index); + +/** + * @brief Mute / lock flags. + */ +int oaknode_track_get_muted(OakNodeTrack *track, int *muted); +int oaknode_track_set_muted(OakNodeTrack *track, int muted); +int oaknode_track_get_locked(OakNodeTrack *track, int *locked); +int oaknode_track_set_locked(OakNodeTrack *track, int locked); + +/** + * @brief Track reference as a (type, index) pair (olive::Track::Reference). + */ +int oaknode_track_get_reference(OakNodeTrack *track, int *type, int *index); + +/** + * @brief Total length of the track (end of the last block). + */ +int oaknode_track_get_length(OakNodeTrack *track, int *numerator, + int *denominator); + +/** + * @brief Owning sequence as a borrowed handle (NULL when trackless). + */ +int oaknode_track_get_sequence(OakNodeTrack *track, OakNodeSequence **out); + +/* ------------------------------------------------------- Track blocks */ + +/** + * @brief Number of blocks on the track. + */ +int oaknode_track_get_block_count(OakNodeTrack *track, int *count); + +/** + * @brief Borrowed handle to the block at `index`. + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. + */ +int oaknode_track_get_block_at(OakNodeTrack *track, int index, + OakNodeBlock **out); + +/** + * @brief Append/prepend/insert primitives (olive::Track::*_block). + * + * The track takes over graph membership of the block; the block must have + * a valid length before insertion. + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_track_append_block(OakNodeTrack *track, OakNodeBlock *block); +int oaknode_track_prepend_block(OakNodeTrack *track, OakNodeBlock *block); +int oaknode_track_insert_block_at_index(OakNodeTrack *track, + OakNodeBlock *block, int index); +int oaknode_track_insert_block_after(OakNodeTrack *track, OakNodeBlock *block, + OakNodeBlock *before); +int oaknode_track_insert_block_before(OakNodeTrack *track, OakNodeBlock *block, + OakNodeBlock *after); + +/** + * @brief Remove `block` and shift all subsequent blocks earlier + * (olive::Track::ripple_remove_block). The block is NOT deleted; ownership + * returns to the caller. + */ +int oaknode_track_ripple_remove_block(OakNodeTrack *track, OakNodeBlock *block); + +/** + * @brief Replace `old_block` with `new_block`; both must have equal lengths. + */ +int oaknode_track_replace_block(OakNodeTrack *track, OakNodeBlock *old_block, + OakNodeBlock *new_block); + +/** + * @brief Index of `block` in the track's block array, or OAKNODE_E_NOT_FOUND. + */ +int oaknode_track_get_block_index(OakNodeTrack *track, OakNodeBlock *block, + int *index); + +/** + * @brief Block strictly containing `time` (in < time < out), or + * OAKNODE_E_NOT_FOUND. + */ +int oaknode_track_get_block_containing_time(OakNodeTrack *track, int numerator, + int denominator, + OakNodeBlock **out); + +/** + * @brief Block visible at `time` (in <= time < out), or OAKNODE_E_NOT_FOUND. + */ +int oaknode_track_get_visible_block_at_time(OakNodeTrack *track, int numerator, + int denominator, + OakNodeBlock **out); + +/** + * @brief Whether the [in, out) range holds no block or only a gap + * (olive::Track::is_range_free). `is_free` receives 1/0. + */ +int oaknode_track_is_range_free(OakNodeTrack *track, int in_num, int in_den, + int out_num, int out_den, int *is_free); + +/* ------------------------------------------------------------ TrackList */ + +/** + * @brief Track list type (OakNodeTrackType values). + */ +int oaknode_tracklist_get_type(OakNodeTrackList *list, int *type); + +/** + * @brief Number of connected tracks. + */ +int oaknode_tracklist_get_track_count(OakNodeTrackList *list, int *count); + +/** + * @brief Borrowed handle to the track at `index`. + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. + */ +int oaknode_tracklist_get_track_at(OakNodeTrackList *list, int index, + OakNodeTrack **out); + +/** + * @brief Combined length of the longest track in the list. + */ +int oaknode_tracklist_get_total_length(OakNodeTrackList *list, int *numerator, + int *denominator); + +/** + * @brief Size of the underlying input array (>= track count; may contain + * disconnected slots). + */ +int oaknode_tracklist_get_array_size(OakNodeTrackList *list, int *size); + +/** + * @brief Add `track` to the list (non-undoable primitive). + * + * Mirrors the graph steps of TimelineAddTrackCommand::redo() minus the + * auto-merge: the track is parented to the list's graph (when any), + * inherits the previous track's height, a new array slot is appended and + * the track is connected to it. The sequence's flat track cache and + * lengths are refreshed before returning. + * + * @return OAKNODE_OK or OAKNODE_E_INVALID. + */ +int oaknode_tracklist_add_track(OakNodeTrackList *list, OakNodeTrack *track); + +/** + * @brief Remove `track` from the list (non-undoable primitive). + * + * Disconnects the track from its array slot and removes the slot + * (Node::input_array_remove). The track is NOT deleted; ownership returns + * to the caller. + * + * @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND. + */ +int oaknode_tracklist_remove_track(OakNodeTrackList *list, + OakNodeTrack *track); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_TRACK_H diff --git a/include/node/traverser.h b/include/node/traverser.h new file mode 100644 index 000000000..9f38d889c --- /dev/null +++ b/include/node/traverser.h @@ -0,0 +1,130 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_TRAVERSER_H +#define OAK_EDITOR_NODE_TRAVERSER_H + +#include + +#include "node/error.h" +#include "node/node.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file traverser.h + * @brief C ABI for olive::NodeTraverser (src/node/src/traverser.h), + * limited to database generation: generating the value database of a node + * over a time range and enumerating its rows. + * + * The base NodeTraverser resolves no render jobs (textures/samples stay + * dummy); only value-producing nodes are meaningful here. + */ + +/** + * @brief Opaque traverser handle (olive::NodeTraverser). + */ +typedef struct OakNodeTraverser OakNodeTraverser; + +/** + * @brief Opaque value-database handle (an owned copy of an + * olive::NodeValueDatabase). Release with + * oaknode_traverser_database_free(). + */ +typedef struct OakNodeValueDatabase OakNodeValueDatabase; + +/** + * @brief Create a traverser. + * + * @return Traverser handle, or NULL on allocation failure. + */ +OakNodeTraverser *oaknode_traverser_init(void); + +/** + * @brief Destroy a traverser. NULL is a no-op. + */ +void oaknode_traverser_free(OakNodeTraverser *traverser); + +/** + * @brief Generate the value database of `node` over the time range + * [`in_num`/`in_den`, `out_num`/`out_den`) seconds + * (NodeTraverser::generate_database()). + * + * `out_db` receives an owned database handle. + * + * @return OAKNODE_OK or a negative OAKNODE_E_* error code. + */ +int oaknode_traverser_generate_database(OakNodeTraverser *traverser, + OakNodeNode *node, int64_t in_num, + int64_t in_den, int64_t out_num, + int64_t out_den, + OakNodeValueDatabase **out_db); + +/** + * @brief Destroy a database handle. NULL is a no-op. + */ +void oaknode_traverser_database_free(OakNodeValueDatabase *db); + +/** + * @brief Number of rows (input tables) in the database. + */ +int oaknode_traverser_database_row_count(const OakNodeValueDatabase *db, + int *out_count); + +/** + * @brief The input id (key) of the row at `index`. Two-stage getter; + * OAKNODE_E_NOT_FOUND for an out-of-range index. + */ +int oaknode_traverser_database_row_key_at(const OakNodeValueDatabase *db, + int index, char *buf, int buf_size); + +/** + * @brief Number of values in the row named `key`. + * OAKNODE_E_NOT_FOUND for an unknown key. + */ +int oaknode_traverser_database_row_value_count(const OakNodeValueDatabase *db, + const char *key, + int *out_count); + +/** + * @brief Read the value at `index` of row `key` mapped into `out`. + * Values without a POD representation fail with OAKNODE_E_FAILED; + * OAKNODE_E_NOT_FOUND for an unknown key or out-of-range index. + */ +int oaknode_traverser_database_value_at(const OakNodeValueDatabase *db, + const char *key, int index, + oaknode_value *out); + +/** + * @brief Read the value at `index` of row `key` as a string + * (NodeValue::value_to_string()). Two-stage getter; + * OAKNODE_E_NOT_FOUND for an unknown key or out-of-range index. + */ +int oaknode_traverser_database_value_string_at(const OakNodeValueDatabase *db, + const char *key, int index, + char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif + +#endif //OAK_EDITOR_NODE_TRAVERSER_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e089491e..c0819a57e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +1,2 @@ add_subdirectory(common) -add_subdirectory(undo) \ No newline at end of file +add_subdirectory(undo)add_subdirectory(node) diff --git a/src/common/c_api/CMakeLists.txt b/src/common/c_api/CMakeLists.txt index 28451f1f9..7f670150d 100644 --- a/src/common/c_api/CMakeLists.txt +++ b/src/common/c_api/CMakeLists.txt @@ -1,6 +1,9 @@ target_sources(oakcommon PRIVATE + colortransform.cpp commandlineparser.cpp current.cpp + subtitleparams.cpp + videoparams.cpp debug.cpp dropworkflowbehavior.cpp ffmpegutils.cpp diff --git a/src/common/c_api/colortransform.cpp b/src/common/c_api/colortransform.cpp new file mode 100644 index 000000000..a6ebce08a --- /dev/null +++ b/src/common/c_api/colortransform.cpp @@ -0,0 +1,125 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/colortransform.h" + +#include + +#include "../src/colortransform.h" + +struct OakCommonColorTransform { + olive::ColorTransform impl; +}; + +static int copy_string(const std::string &value, char *buf, int buf_size) +{ + int needed = (int)value.size() + 1; + if (buf && buf_size >= needed) + memcpy(buf, value.c_str(), needed); + return needed; +} + +OakCommonColorTransform *oakcommon_colortransform_init_output( + const char *output) +{ + if (!output) + return nullptr; + try { + return new OakCommonColorTransform{ + olive::ColorTransform(std::string(output))}; + } catch (...) { + return nullptr; + } +} + +OakCommonColorTransform *oakcommon_colortransform_init_display( + const char *display, const char *view, const char *look) +{ + if (!display || !view || !look) + return nullptr; + try { + return new OakCommonColorTransform{olive::ColorTransform( + std::string(display), std::string(view), std::string(look))}; + } catch (...) { + return nullptr; + } +} + +void oakcommon_colortransform_free(OakCommonColorTransform *transform) +{ + delete transform; +} + +int oakcommon_colortransform_is_display(OakCommonColorTransform *transform, + int *is_display) +{ + if (!transform || !is_display) + return OAKCOMMON_E_INVALID; + *is_display = transform->impl.is_display() ? 1 : 0; + return OAKCOMMON_OK; +} + +int oakcommon_colortransform_get_display(OakCommonColorTransform *transform, + char *buf, int buf_size) +{ + if (!transform) + return OAKCOMMON_E_INVALID; + try { + return copy_string(transform->impl.display(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_colortransform_get_output(OakCommonColorTransform *transform, + char *buf, int buf_size) +{ + if (!transform) + return OAKCOMMON_E_INVALID; + try { + return copy_string(transform->impl.output(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_colortransform_get_view(OakCommonColorTransform *transform, + char *buf, int buf_size) +{ + if (!transform) + return OAKCOMMON_E_INVALID; + try { + return copy_string(transform->impl.view(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_colortransform_get_look(OakCommonColorTransform *transform, + char *buf, int buf_size) +{ + if (!transform) + return OAKCOMMON_E_INVALID; + try { + return copy_string(transform->impl.look(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/subtitleparams.cpp b/src/common/c_api/subtitleparams.cpp new file mode 100644 index 000000000..df9f77fb6 --- /dev/null +++ b/src/common/c_api/subtitleparams.cpp @@ -0,0 +1,223 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/subtitleparams.h" + +#include + +#include "../src/subtitleparams.h" + +struct OakCommonSubtitleParams { + olive::SubtitleParams impl; +}; + +namespace +{ + +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int needed = (int)value.size() + 1; + if (buf && buf_size >= needed) + memcpy(buf, value.c_str(), needed); + return needed; +} + +} // namespace + +OakCommonSubtitleParams *oakcommon_subtitleparams_init(void) +{ + try { + return new OakCommonSubtitleParams{olive::SubtitleParams()}; + } catch (...) { + return nullptr; + } +} + +void oakcommon_subtitleparams_free(OakCommonSubtitleParams *params) +{ + delete params; +} + +int oakcommon_subtitleparams_get_stream_index( + OakCommonSubtitleParams *params, int *index) +{ + if (!params || !index) + return OAKCOMMON_E_INVALID; + *index = params->impl.stream_index(); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_set_stream_index( + OakCommonSubtitleParams *params, int index) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_stream_index(index); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_get_enabled(OakCommonSubtitleParams *params, + int *enabled) +{ + if (!params || !enabled) + return OAKCOMMON_E_INVALID; + *enabled = params->impl.enabled() ? 1 : 0; + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_set_enabled(OakCommonSubtitleParams *params, + int enabled) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_enabled(enabled != 0); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_is_valid(OakCommonSubtitleParams *params, + int *is_valid) +{ + if (!params || !is_valid) + return OAKCOMMON_E_INVALID; + *is_valid = params->impl.is_valid() ? 1 : 0; + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_count(OakCommonSubtitleParams *params, int *count) +{ + if (!params || !count) + return OAKCOMMON_E_INVALID; + *count = (int)params->impl.size(); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_duration(OakCommonSubtitleParams *params, + int *numerator, int *denominator) +{ + if (!params || !numerator || !denominator) + return OAKCOMMON_E_INVALID; + olive::core::Rational d = params->impl.duration(); + *numerator = d.numerator(); + *denominator = d.denominator(); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_add_subtitle(OakCommonSubtitleParams *params, + int in_num, int in_den, int out_num, + int out_den, const char *text) +{ + if (!params || !text) + return OAKCOMMON_E_INVALID; + try { + params->impl.push_back(olive::Subtitle( + olive::core::TimeRange(olive::core::Rational(in_num, in_den), + olive::core::Rational(out_num, out_den)), + text)); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_subtitleparams_clear(OakCommonSubtitleParams *params) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.clear(); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_get_subtitle(OakCommonSubtitleParams *params, + int index, int *in_num, int *in_den, + int *out_num, int *out_den) +{ + if (!params || !in_num || !in_den || !out_num || !out_den) + return OAKCOMMON_E_INVALID; + if (index < 0 || index >= (int)params->impl.size()) + return OAKCOMMON_E_NOT_FOUND; + const olive::Subtitle &s = params->impl.at(index); + olive::core::Rational in = s.time().in(); + olive::core::Rational out = s.time().out(); + *in_num = in.numerator(); + *in_den = in.denominator(); + *out_num = out.numerator(); + *out_den = out.denominator(); + return OAKCOMMON_OK; +} + +int oakcommon_subtitleparams_get_subtitle_text(OakCommonSubtitleParams *params, + int index, char *buf, + int buf_size) +{ + if (!params) + return OAKCOMMON_E_INVALID; + if (index < 0 || index >= (int)params->impl.size()) + return OAKCOMMON_E_NOT_FOUND; + try { + return copy_string(params->impl.at(index).text(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_subtitleparams_generate_ass_header(char *buf, int buf_size) +{ + try { + return copy_string(olive::SubtitleParams::generate_ass_header(), buf, + buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params, + const char *xml) +{ + if (!params || !xml) + return OAKCOMMON_E_INVALID; + try { + olive::XmlStreamReader reader(xml); + if (reader.has_error()) + return OAKCOMMON_E_FAILED; + // Position on the root element; load() consumes its children. + if (!olive::xml_read_next_start_element(&reader)) + return OAKCOMMON_E_FAILED; + params->impl.load(&reader); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_subtitleparams_save_xml(OakCommonSubtitleParams *params, + char *buf, int buf_size) +{ + if (!params) + return OAKCOMMON_E_INVALID; + try { + olive::XmlStreamWriter writer; + writer.write_start_element("subtitleparams"); + params->impl.save(&writer); + writer.write_end_element(); + return copy_string(writer.output(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/c_api/videoparams.cpp b/src/common/c_api/videoparams.cpp new file mode 100644 index 000000000..31b9cea12 --- /dev/null +++ b/src/common/c_api/videoparams.cpp @@ -0,0 +1,478 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "common/videoparams.h" + +#include + +#include "../src/videoparams.h" + +struct OakCommonVideoParams { + olive::VideoParams impl; +}; + +namespace +{ + +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int needed = (int)value.size() + 1; + if (buf && buf_size >= needed) + memcpy(buf, value.c_str(), needed); + return needed; +} + +int get_rational(const olive::core::Rational &r, int *numerator, + int *denominator) +{ + if (!numerator || !denominator) + return OAKCOMMON_E_INVALID; + *numerator = r.numerator(); + *denominator = r.denominator(); + return OAKCOMMON_OK; +} + +} // namespace + +OakCommonVideoParams *oakcommon_videoparams_init(void) +{ + try { + return new OakCommonVideoParams{olive::VideoParams()}; + } catch (...) { + return nullptr; + } +} + +OakCommonVideoParams *oakcommon_videoparams_init_basic( + int width, int height, int pixel_format, int nb_channels, + int pixel_aspect_num, int pixel_aspect_den, int interlacing, int divider) +{ + try { + return new OakCommonVideoParams{olive::VideoParams( + width, height, + static_cast(pixel_format), + nb_channels, + olive::core::Rational(pixel_aspect_num, pixel_aspect_den), + static_cast(interlacing), + divider)}; + } catch (...) { + return nullptr; + } +} + +OakCommonVideoParams *oakcommon_videoparams_init_with_time_base( + int width, int height, int time_base_num, int time_base_den, + int pixel_format, int nb_channels, int pixel_aspect_num, + int pixel_aspect_den, int interlacing, int divider) +{ + try { + return new OakCommonVideoParams{olive::VideoParams( + width, height, + olive::core::Rational(time_base_num, time_base_den), + static_cast(pixel_format), + nb_channels, + olive::core::Rational(pixel_aspect_num, pixel_aspect_den), + static_cast(interlacing), + divider)}; + } catch (...) { + return nullptr; + } +} + +void oakcommon_videoparams_free(OakCommonVideoParams *params) +{ + delete params; +} + +#define OAKCOMMON_VIDEOPARAMS_INT_GETTER(name, expr) \ + int oakcommon_videoparams_get_##name(OakCommonVideoParams *params, \ + int *out) \ + { \ + if (!params || !out) \ + return OAKCOMMON_E_INVALID; \ + *out = (expr); \ + return OAKCOMMON_OK; \ + } + +#define OAKCOMMON_VIDEOPARAMS_INT_SETTER(name, stmt) \ + int oakcommon_videoparams_set_##name(OakCommonVideoParams *params, \ + int value) \ + { \ + if (!params) \ + return OAKCOMMON_E_INVALID; \ + stmt; \ + return OAKCOMMON_OK; \ + } + +OAKCOMMON_VIDEOPARAMS_INT_GETTER(width, params->impl.width()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(width, params->impl.set_width(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(height, params->impl.height()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(height, params->impl.set_height(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(depth, params->impl.depth()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(depth, params->impl.set_depth(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_3d, params->impl.is_3d() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(format, static_cast(params->impl.format())) +OAKCOMMON_VIDEOPARAMS_INT_SETTER( + format, params->impl.set_format( + static_cast(value))) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(channel_count, params->impl.channel_count()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(channel_count, + params->impl.set_channel_count(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(interlacing, + static_cast(params->impl.interlacing())) +OAKCOMMON_VIDEOPARAMS_INT_SETTER( + interlacing, + params->impl.set_interlacing( + static_cast(value))) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(divider, params->impl.divider()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(divider, params->impl.set_divider(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(enabled, params->impl.enabled() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(enabled, params->impl.set_enabled(value != 0)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(stream_index, params->impl.stream_index()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(stream_index, + params->impl.set_stream_index(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(video_type, + static_cast(params->impl.video_type())) +OAKCOMMON_VIDEOPARAMS_INT_SETTER( + video_type, + params->impl.set_video_type(static_cast(value))) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(premultiplied_alpha, + params->impl.premultiplied_alpha() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(premultiplied_alpha, + params->impl.set_premultiplied_alpha( + value != 0)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_range, + static_cast(params->impl.color_range())) +OAKCOMMON_VIDEOPARAMS_INT_SETTER( + color_range, params->impl.set_color_range( + static_cast(value))) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_primaries, + params->impl.color_primaries()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(color_primaries, + params->impl.set_color_primaries(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_transfer, params->impl.color_transfer()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(color_transfer, + params->impl.set_color_transfer(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(square_pixel_width, + params->impl.square_pixel_width()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_width, + params->impl.effective_width()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_height, + params->impl.effective_height()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_depth, + params->impl.effective_depth()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_valid, params->impl.is_valid() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(bytes_per_channel, + params->impl.get_bytes_per_channel()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(bytes_per_pixel, + params->impl.get_bytes_per_pixel()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(buffer_size, params->impl.get_buffer_size()) + +int oakcommon_videoparams_get_x(OakCommonVideoParams *params, float *x) +{ + if (!params || !x) + return OAKCOMMON_E_INVALID; + *x = params->impl.x(); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_set_x(OakCommonVideoParams *params, float x) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_x(x); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_y(OakCommonVideoParams *params, float *y) +{ + if (!params || !y) + return OAKCOMMON_E_INVALID; + *y = params->impl.y(); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_set_y(OakCommonVideoParams *params, float y) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_y(y); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_start_time(OakCommonVideoParams *params, + int64_t *start_time) +{ + if (!params || !start_time) + return OAKCOMMON_E_INVALID; + *start_time = params->impl.start_time(); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_set_start_time(OakCommonVideoParams *params, + int64_t start_time) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_start_time(start_time); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_duration(OakCommonVideoParams *params, + int64_t *duration) +{ + if (!params || !duration) + return OAKCOMMON_E_INVALID; + *duration = params->impl.duration(); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_set_duration(OakCommonVideoParams *params, + int64_t duration) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_duration(duration); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_time_base(OakCommonVideoParams *params, + int *numerator, int *denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + return get_rational(params->impl.time_base(), numerator, denominator); +} + +int oakcommon_videoparams_set_time_base(OakCommonVideoParams *params, + int numerator, int denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_time_base(olive::core::Rational(numerator, denominator)); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_frame_rate(OakCommonVideoParams *params, + int *numerator, int *denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + return get_rational(params->impl.frame_rate(), numerator, denominator); +} + +int oakcommon_videoparams_set_frame_rate(OakCommonVideoParams *params, + int numerator, int denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_frame_rate(olive::core::Rational(numerator, denominator)); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_frame_rate_as_time_base(OakCommonVideoParams *params, + int *numerator, + int *denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + return get_rational(params->impl.frame_rate_as_time_base(), numerator, + denominator); +} + +int oakcommon_videoparams_get_pixel_aspect_ratio(OakCommonVideoParams *params, + int *numerator, + int *denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + return get_rational(params->impl.pixel_aspect_ratio(), numerator, + denominator); +} + +int oakcommon_videoparams_set_pixel_aspect_ratio(OakCommonVideoParams *params, + int numerator, int denominator) +{ + if (!params) + return OAKCOMMON_E_INVALID; + params->impl.set_pixel_aspect_ratio( + olive::core::Rational(numerator, denominator)); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_colorspace(OakCommonVideoParams *params, + char *buf, int buf_size) +{ + if (!params) + return OAKCOMMON_E_INVALID; + try { + return copy_string(params->impl.colorspace(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_videoparams_set_colorspace(OakCommonVideoParams *params, + const char *colorspace) +{ + if (!params || !colorspace) + return OAKCOMMON_E_INVALID; + params->impl.set_colorspace(colorspace); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_get_time_in_timebase_units( + OakCommonVideoParams *params, int time_num, int time_den, + int64_t *timestamp) +{ + if (!params || !timestamp) + return OAKCOMMON_E_INVALID; + *timestamp = params->impl.get_time_in_timebase_units( + olive::core::Rational(time_num, time_den)); + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_equals(OakCommonVideoParams *params, + OakCommonVideoParams *other, int *equal) +{ + if (!params || !other || !equal) + return OAKCOMMON_E_INVALID; + *equal = (params->impl == other->impl) ? 1 : 0; + return OAKCOMMON_OK; +} + +int oakcommon_videoparams_load_xml(OakCommonVideoParams *params, + const char *xml) +{ + if (!params || !xml) + return OAKCOMMON_E_INVALID; + try { + olive::XmlStreamReader reader(xml); + if (reader.has_error()) + return OAKCOMMON_E_FAILED; + // Position on the root element; load() consumes its children. + if (!olive::xml_read_next_start_element(&reader)) + return OAKCOMMON_E_FAILED; + params->impl.load(&reader); + return OAKCOMMON_OK; + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_videoparams_save_xml(OakCommonVideoParams *params, char *buf, + int buf_size) +{ + if (!params) + return OAKCOMMON_E_INVALID; + try { + olive::XmlStreamWriter writer; + writer.write_start_element("videoparams"); + params->impl.save(&writer); + writer.write_end_element(); + return copy_string(writer.output(), buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_videoparams_get_bytes_per_channel_for_format(int pixel_format) +{ + return olive::VideoParams::get_bytes_per_channel( + static_cast(pixel_format)); +} + +int oakcommon_videoparams_get_bytes_per_pixel_for_format(int pixel_format, + int channels) +{ + return olive::VideoParams::get_bytes_per_pixel( + static_cast(pixel_format), channels); +} + +int oakcommon_videoparams_calculate_buffer_size(int width, int height, + int pixel_format, int channels) +{ + return olive::VideoParams::get_buffer_size( + width, height, + static_cast(pixel_format), channels); +} + +int oakcommon_videoparams_format_is_float(int pixel_format) +{ + return olive::VideoParams::format_is_float( + static_cast(pixel_format)) ? + 1 : + 0; +} + +int oakcommon_videoparams_generate_auto_divider(int64_t width, int64_t height) +{ + return olive::VideoParams::generate_auto_divider(width, height); +} + +int oakcommon_videoparams_get_scaled_dimension(int dimension, int divider) +{ + return olive::VideoParams::get_scaled_dimension(dimension, divider); +} + +int oakcommon_videoparams_get_divider_for_target_resolution(int src_width, + int src_height, + int dst_width, + int dst_height) +{ + return olive::VideoParams::get_divider_for_target_resolution( + src_width, src_height, dst_width, dst_height); +} + +int oakcommon_videoparams_get_name_for_divider(int divider, char *buf, + int buf_size) +{ + try { + return copy_string(olive::VideoParams::get_name_for_divider(divider), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_videoparams_get_format_name(int pixel_format, char *buf, + int buf_size) +{ + try { + return copy_string(olive::VideoParams::get_format_name( + static_cast( + pixel_format)), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} + +int oakcommon_videoparams_frame_rate_to_string(int numerator, int denominator, + char *buf, int buf_size) +{ + try { + return copy_string(olive::VideoParams::frame_rate_to_string( + olive::core::Rational(numerator, denominator)), + buf, buf_size); + } catch (...) { + return OAKCOMMON_E_FAILED; + } +} diff --git a/src/common/src/CMakeLists.txt b/src/common/src/CMakeLists.txt index db2868875..3762a5c56 100644 --- a/src/common/src/CMakeLists.txt +++ b/src/common/src/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(oakcommon SHARED commandlineparser.cpp commandlineparser.h + colortransform.h current.cpp current.h debug.cpp @@ -25,6 +26,11 @@ add_library(oakcommon SHARED decibel.h define.h dropworkflowbehavior.h + loopmode.h + subtitleparams.cpp + subtitleparams.h + videoparams.cpp + videoparams.h ffmpegutils.cpp ffmpegutils.h filefunctions.cpp diff --git a/src/common/src/colortransform.h b/src/common/src/colortransform.h new file mode 100644 index 000000000..d409773fa --- /dev/null +++ b/src/common/src/colortransform.h @@ -0,0 +1,91 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_COLORTRANSFORM_H +#define OAK_COLORTRANSFORM_H + +#include + +namespace olive +{ + +/** + * @brief Describes a color transform: either a plain output colorspace or a + * display/view/look triplet (sunk from engine/render/colortransform.h, + * QString replaced with std::string). + */ +class ColorTransform { +public: + ColorTransform() + { + is_display_ = false; + } + + ColorTransform(const std::string &output) + { + is_display_ = false; + output_ = output; + } + + ColorTransform(const std::string &display, const std::string &view, + const std::string &look) + { + is_display_ = true; + output_ = display; + view_ = view; + look_ = look; + } + + bool is_display() const + { + return is_display_; + } + + const std::string &display() const + { + return output_; + } + + const std::string &output() const + { + return output_; + } + + const std::string &view() const + { + return view_; + } + + const std::string &look() const + { + return look_; + } + +private: + std::string output_; + + bool is_display_; + std::string view_; + std::string look_; +}; + +} + +#endif // OAK_COLORTRANSFORM_H diff --git a/src/common/src/loopmode.h b/src/common/src/loopmode.h new file mode 100644 index 000000000..ee9462543 --- /dev/null +++ b/src/common/src/loopmode.h @@ -0,0 +1,34 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LOOPMODE_H +#define OAK_LOOPMODE_H + +namespace olive +{ + +/** + * @brief Playback loop mode (sunk from engine/render/loopmode.h). + */ +enum class LoopMode { k_loop_mode_off, k_loop_mode_loop, k_loop_mode_clamp }; + +} + +#endif // OAK_LOOPMODE_H diff --git a/src/common/src/subtitleparams.cpp b/src/common/src/subtitleparams.cpp new file mode 100644 index 000000000..8355bee50 --- /dev/null +++ b/src/common/src/subtitleparams.cpp @@ -0,0 +1,172 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "subtitleparams.h" + +#include + +namespace olive +{ + +std::string SubtitleParams::generate_ass_header() +{ + // NOTE: We'll probably implement more customization as we support ASS better. Right now, we only + // natively support SRT and only make this header because FFmpeg requires it. + static const int k_ass_default_play_res_x = 384; + static const int k_ass_default_play_res_y = 288; + static const char *k_ass_default_font = "Arial"; + static const int k_ass_default_font_size = 16; + static const int k_ass_default_primary_color = 0xFFFFFF; // White + static const int k_ass_default_secondary_color = 0xFFFFFF; // White + static const int k_ass_default_outline_color = 0x000000; // Black + static const int k_ass_default_back_color = 0x000000; // Black + static const int k_ass_bold = 0; + static const int k_ass_italic = 0; + static const int k_ass_underline = 0; + static const int k_ass_strike = 0; + static const int k_ass_border_style = 1; + static const int k_ass_alignment = 2; + + std::string ass_code; + char buf[256]; + + // Header info + // De-Qt note: the original used QCoreApplication::applicationName()/ + // applicationVersion(); oakcommon has no application object, so a fixed + // generator name is emitted instead. + ass_code.append("[Script Info]\r\n"); + ass_code.append("; Script generated by Oak\r\n"); + ass_code.append("ScriptType: v4.00+\r\n"); + std::snprintf(buf, sizeof(buf), "PlayResX: %d\r\n", k_ass_default_play_res_x); + ass_code.append(buf); + std::snprintf(buf, sizeof(buf), "PlayResY: %d\r\n", k_ass_default_play_res_y); + ass_code.append(buf); + ass_code.append("ScaledBorderAndShadow: yes\r\n"); + ass_code.append("\r\n"); + + // ASSv4 header + ass_code.append("[V4+ Styles]\r\n"); + ass_code.append("Format: Name, "); + ass_code.append("Fontname, Fontsize, "); + ass_code.append("PrimaryColour, SecondaryColour, OutlineColour, BackColour, "); + ass_code.append("Bold, Italic, Underline, StrikeOut, "); + ass_code.append("ScaleX, ScaleY, "); + ass_code.append("Spacing, Angle, "); + ass_code.append("BorderStyle, Outline, Shadow, "); + ass_code.append("Alignment, MarginL, MarginR, MarginV, "); + ass_code.append("Encoding\r\n"); + ass_code.append("Style: "); + + // Name + ass_code.append("Default,"); + + // Font{name,size} + std::snprintf(buf, sizeof(buf), "%s,%d,", k_ass_default_font, + k_ass_default_font_size); + ass_code.append(buf); + + // {Primary,Secondary,Outline,Back}Colour + std::snprintf(buf, sizeof(buf), "&H%X,&H%X,&H%X,&H%X,", + k_ass_default_primary_color, k_ass_default_secondary_color, + k_ass_default_outline_color, k_ass_default_back_color); + ass_code.append(buf); + + // Bold, Italic, Underline, StrikeOut + std::snprintf(buf, sizeof(buf), "%d,%d,%d,%d,", k_ass_bold, k_ass_italic, + k_ass_underline, k_ass_strike); + ass_code.append(buf); + + // Scale{X,Y} + ass_code.append("100,100,"); + + // Spacing, Angle + ass_code.append("0,0,"); + + // BorderStyle, Outline, Shadow + std::snprintf(buf, sizeof(buf), "%d,1,0,", k_ass_border_style); + ass_code.append(buf); + + // Alignment, Margin[LRV] + std::snprintf(buf, sizeof(buf), "%d,10,10,10,", k_ass_alignment); + ass_code.append(buf); + + // Encoding + ass_code.append("0\r\n"); + ass_code.append("\r\n"); + ass_code.append("[Events]\r\n"); + ass_code.append( + "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n"); + + return ass_code; +} + +void SubtitleParams::load(XmlStreamReader *reader) +{ + this->clear(); + + while (xml_read_next_start_element(reader)) { + const std::string &name = reader->name(); + if (name == "streamindex") { + set_stream_index(std::stoi(reader->read_element_text())); + } else if (name == "enabled") { + set_enabled(std::stoi(reader->read_element_text())); + } else if (name == "subtitles") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "subtitle") { + Rational in, out; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "in") { + in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + out = Rational::from_string(attr.value); + } + } + + std::string text = reader->read_element_text(); + + this->push_back(Subtitle(TimeRange(in, out), text)); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } +} + +void SubtitleParams::save(XmlStreamWriter *writer) const +{ + writer->write_text_element("streamindex", std::to_string(stream_index_)); + writer->write_text_element("enabled", std::to_string(enabled_)); + + writer->write_start_element("subtitles"); + for (auto it = this->cbegin(); it != this->cend(); it++) { + writer->write_start_element("subtitle"); + writer->write_attribute("in", it->time().in().to_string()); + writer->write_attribute("out", it->time().out().to_string()); + writer->write_characters(it->text()); + writer->write_end_element(); // subtitle + } + writer->write_end_element(); // subtitles +} + +} diff --git a/src/common/src/subtitleparams.h b/src/common/src/subtitleparams.h new file mode 100644 index 000000000..4c44e887d --- /dev/null +++ b/src/common/src/subtitleparams.h @@ -0,0 +1,142 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SUBTITLEPARAMS_H +#define OAK_SUBTITLEPARAMS_H + +#include +#include + +#include + +#include "xmlutils.h" + +using namespace olive::core; + +namespace olive +{ + +/** + * @brief A single subtitle entry (sunk from engine/render/subtitleparams.h, + * QString replaced with std::string). + */ +class Subtitle { +public: + Subtitle() = default; + + Subtitle(const TimeRange &time, const std::string &text) + : range_(time) + , text_(text) + { + } + + const TimeRange &time() const + { + return range_; + } + void set_time(const TimeRange &t) + { + range_ = t; + } + + const std::string &text() const + { + return text_; + } + void set_text(const std::string &t) + { + text_ = t; + } + + // Value equality (time range + text). Required by olive::Variant's + // custom-type operator== (SubtitleParams is stored as a Variant value). + bool operator==(const Subtitle &rhs) const + { + return range_ == rhs.range_ && text_ == rhs.text_; + } + bool operator!=(const Subtitle &rhs) const + { + return !(*this == rhs); + } + +private: + TimeRange range_; + + std::string text_; +}; + +/** + * @brief Subtitle track parameter set (sunk from engine/render/subtitleparams.h). + */ +class SubtitleParams : public std::vector { +public: + SubtitleParams() + { + stream_index_ = 0; + enabled_ = true; + } + + static std::string generate_ass_header(); + + void load(XmlStreamReader *reader); + + void save(XmlStreamWriter *writer) const; + + bool is_valid() const + { + return !this->empty(); + } + + Rational duration() const + { + if (this->empty()) { + return 0; + } else { + return back().time().out(); + } + } + + int stream_index() const + { + return stream_index_; + } + void set_stream_index(int i) + { + stream_index_ = i; + } + + bool enabled() const + { + return enabled_; + } + void set_enabled(bool e) + { + enabled_ = e; + } + +private: + int stream_index_; + + bool enabled_; +}; + +} + +#endif // OAK_SUBTITLEPARAMS_H diff --git a/src/common/src/videoparams.cpp b/src/common/src/videoparams.cpp new file mode 100644 index 000000000..0385107a7 --- /dev/null +++ b/src/common/src/videoparams.cpp @@ -0,0 +1,466 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "videoparams.h" + +#include +#include + +#include + +#include "ofxImageEffect.h" + +namespace olive +{ + +const int VideoParams::k_internal_channel_count = k_rgba_channel_count; + +const Rational VideoParams::k_pixel_aspect_square(1); +const Rational VideoParams::k_pixel_aspect_ntsc_standard(8, 9); +const Rational VideoParams::k_pixel_aspect_ntsc_widescreen(32, 27); +const Rational VideoParams::k_pixel_aspect_pal_standard(16, 15); +const Rational VideoParams::k_pixel_aspect_pal_widescreen(64, 45); +const Rational VideoParams::k_pixel_aspect1080_anamorphic(4, 3); + +const std::vector VideoParams::k_supported_frame_rates = { + Rational(10, 1), // 10 FPS + Rational(15, 1), // 15 FPS + Rational(24000, 1001), // 23.976 FPS + Rational(24, 1), // 24 FPS + Rational(25, 1), // 25 FPS + Rational(30000, 1001), // 29.97 FPS + Rational(30, 1), // 30 FPS + Rational(48000, 1001), // 47.952 FPS + Rational(48, 1), // 48 FPS + Rational(50, 1), // 50 FPS + Rational(60000, 1001), // 59.94 FPS + Rational(60, 1) // 60 FPS +}; + +const std::vector VideoParams::k_supported_dividers = { + 1, 2, 3, 4, 6, 8, 12, 16 +}; + +const std::vector VideoParams::k_standard_pixel_aspects = { + VideoParams::k_pixel_aspect_square, + VideoParams::k_pixel_aspect_ntsc_standard, + VideoParams::k_pixel_aspect_ntsc_widescreen, + VideoParams::k_pixel_aspect_pal_standard, + VideoParams::k_pixel_aspect_pal_widescreen, + VideoParams::k_pixel_aspect1080_anamorphic +}; + +VideoParams::VideoParams() + : width_(0) + , height_(0) + , depth_(0) + , time_base_(0) + , format_(PixelFormat::invalid) + , channel_count_(0) + , pixel_aspect_ratio_(1) + , interlacing_(Interlacing::k_interlace_none) + , divider_(1) +{ + calculate_effective_size(); + validate_pixel_aspect_ratio(); + set_defaults_for_footage(); +} + +VideoParams::VideoParams(int width, int height, PixelFormat format, + int nb_channels, const Rational &pixel_aspect_ratio, + Interlacing interlacing, int divider) + : width_(width) + , height_(height) + , depth_(1) + , format_(format) + , channel_count_(nb_channels) + , pixel_aspect_ratio_(pixel_aspect_ratio) + , interlacing_(interlacing) + , divider_(divider) +{ + calculate_effective_size(); + validate_pixel_aspect_ratio(); + set_defaults_for_footage(); +} + +VideoParams::VideoParams(int width, int height, int depth, PixelFormat format, + int nb_channels, const Rational &pixel_aspect_ratio, + VideoParams::Interlacing interlacing, int divider) + : width_(width) + , height_(height) + , depth_(depth) + , format_(format) + , channel_count_(nb_channels) + , pixel_aspect_ratio_(pixel_aspect_ratio) + , interlacing_(interlacing) + , divider_(divider) +{ + calculate_effective_size(); + validate_pixel_aspect_ratio(); + set_defaults_for_footage(); +} + +void VideoParams::set_channel_count(const std::string &ofx_component) +{ + if (ofx_component == kOfxImageComponentAlpha) { + channel_count_ = 1; + } else if (ofx_component == kOfxImageComponentRGB) { + channel_count_ = k_rgb_channel_count; + } else if (ofx_component == kOfxImageComponentRGBA) { + channel_count_ = k_rgba_channel_count; + } +} + +VideoParams::VideoParams(int width, int height, const Rational &time_base, + PixelFormat format, int nb_channels, + const Rational &pixel_aspect_ratio, + Interlacing interlacing, int divider) + : width_(width) + , height_(height) + , depth_(1) + , time_base_(time_base) + , format_(format) + , channel_count_(nb_channels) + , pixel_aspect_ratio_(pixel_aspect_ratio) + , interlacing_(interlacing) + , divider_(divider) + , frame_rate_(time_base.flipped()) +{ + calculate_effective_size(); + validate_pixel_aspect_ratio(); + set_defaults_for_footage(); +} + +int VideoParams::generate_auto_divider(int64_t width, int64_t height) +{ + const int target_res = 1280 * 720; + + int64_t megapixels = width * height; + + double squared_divider = double(megapixels) / double(target_res); + double divider = std::sqrt(squared_divider); + + if (divider <= k_supported_dividers.front()) { + return k_supported_dividers.front(); + } else if (divider >= k_supported_dividers.back()) { + return k_supported_dividers.back(); + } else { + for (size_t i = 1; i < k_supported_dividers.size(); i++) { + int prev_divider = k_supported_dividers.at(i - 1); + int next_divider = k_supported_dividers.at(i); + + if (divider >= prev_divider && divider <= next_divider) { + double prev_diff = std::fabs(prev_divider - divider); + double next_diff = std::fabs(next_divider - divider); + + if (prev_diff < next_diff) { + return prev_divider; + } else { + return next_divider; + } + } + } + + // Fallback + return 1; + } +} + +bool VideoParams::operator==(const VideoParams &rhs) const +{ + return width() == rhs.width() && height() == rhs.height() && + depth() == rhs.depth() && interlacing() == rhs.interlacing() && + time_base() == rhs.time_base() && format() == rhs.format() && + pixel_aspect_ratio() == rhs.pixel_aspect_ratio() && + divider() == rhs.divider() && channel_count() == rhs.channel_count(); +} + +bool VideoParams::operator!=(const VideoParams &rhs) const +{ + return !(*this == rhs); +} + +int VideoParams::get_bytes_per_channel(PixelFormat format) +{ + switch (format) { + case PixelFormat::invalid: + case PixelFormat::count: + break; + case PixelFormat::u8: + return 1; + case PixelFormat::u10: + return 0; // packed format, use get_bytes_per_pixel instead + case PixelFormat::u16: + case PixelFormat::f16: + return 2; + case PixelFormat::f32: + return 4; + } + + return 0; +} + +int VideoParams::get_bytes_per_pixel(PixelFormat format, int channels) +{ + if (format == PixelFormat::u10) { + // Packed 10-bit RGBA10A2: 4 bytes per RGBA pixel regardless of channel count + return channels == VideoParams::k_rgba_channel_count ? 4 : 0; + } + return get_bytes_per_channel(format) * channels; +} + +std::string VideoParams::get_name_for_divider(int div) +{ + if (div == 1) { + return std::string("Full"); + } else { + return std::string("1/") + std::to_string(div); + } +} + +std::string VideoParams::get_format_name(PixelFormat format) +{ + switch (format) { + case PixelFormat::u8: + return "8-bit"; + case PixelFormat::u10: + return "10-bit Packed"; + case PixelFormat::u16: + return "16-bit Integer"; + case PixelFormat::f16: + return "Half-Float (16-bit)"; + case PixelFormat::f32: + return "Full-Float (32-bit)"; + case PixelFormat::invalid: + case PixelFormat::count: + break; + } + + char buf[64]; + std::snprintf(buf, sizeof(buf), "Unknown (0x%X)", + static_cast(format)); + return buf; +} + +int VideoParams::get_divider_for_target_resolution(int src_width, int src_height, + int dst_width, int dst_height) +{ + int divider = 0; + int test_width, test_height; + + do { + divider++; + + test_width = VideoParams::get_scaled_dimension(src_width, divider); + test_height = VideoParams::get_scaled_dimension(src_height, divider); + } while (test_width > dst_width || test_height > dst_height); + + return divider; +} + +void VideoParams::calculate_effective_size() +{ + effective_width_ = get_scaled_dimension(width(), divider_); + effective_height_ = get_scaled_dimension(height(), divider_); + effective_depth_ = (depth() == 1) ? depth() : + get_scaled_dimension(depth(), divider_); + calculate_square_pixel_width(); +} + +void VideoParams::validate_pixel_aspect_ratio() +{ + if (pixel_aspect_ratio_.isNull()) { + pixel_aspect_ratio_ = 1; + } + calculate_square_pixel_width(); +} + +void VideoParams::set_defaults_for_footage() +{ + enabled_ = true; + stream_index_ = 0; + video_type_ = k_video_type_video; + start_time_ = 0; + duration_ = 0; + premultiplied_alpha_ = false; + x_ = 0; + y_ = 0; + color_range_ = k_color_range_default; +} + +void VideoParams::calculate_square_pixel_width() +{ + if (pixel_aspect_ratio_.denominator() != 0) { + par_width_ = int(std::lround(width_ * pixel_aspect_ratio_.to_double())); + } else { + par_width_ = width_; + } +} + +bool VideoParams::is_valid() const +{ + return (width() > 0 && height() > 0 && !pixel_aspect_ratio_.isNull() && + format_ > PixelFormat::invalid && format_ < PixelFormat::count && + channel_count_ > 0); +} + +std::string VideoParams::frame_rate_to_string(const Rational &frame_rate) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g FPS", frame_rate.to_double()); + return buf; +} + +std::vector VideoParams::get_standard_pixel_aspect_ratio_names() +{ + std::vector strings = { + "Square Pixels (%1)", + "NTSC Standard (%1)", + "NTSC Widescreen (%1)", + "PAL Standard (%1)", + "PAL Widescreen (%1)", + "HD Anamorphic 1080 (%1)" + }; + + // Format each + for (size_t i = 0; i < strings.size(); i++) { + strings.at(i) = format_pixel_aspect_ratio_string( + strings.at(i), k_standard_pixel_aspects.at(i)); + } + + return strings; +} + +std::string VideoParams::format_pixel_aspect_ratio_string( + const std::string &format, const Rational &ratio) +{ + char number[64]; + std::snprintf(number, sizeof(number), "%.4f", ratio.to_double()); + + std::string result = format; + std::string::size_type pos = result.find("%1"); + if (pos != std::string::npos) { + result.replace(pos, 2, number); + } + return result; +} + +int VideoParams::get_scaled_dimension(int dim, int divider) +{ + return dim / divider; +} + +int64_t VideoParams::get_time_in_timebase_units(const Rational &time) const +{ + if (time_base_.isNull()) { + return INT64_MIN; // AV_NOPTS_VALUE + } + + return Timecode::time_to_timestamp(time, time_base_) + start_time_; +} + +void VideoParams::load(XmlStreamReader *reader) +{ + while (xml_read_next_start_element(reader)) { + const std::string &name = reader->name(); + if (name == "width") { + set_width(std::stoi(reader->read_element_text())); + } else if (name == "height") { + set_height(std::stoi(reader->read_element_text())); + } else if (name == "depth") { + set_depth(std::stoi(reader->read_element_text())); + } else if (name == "timebase") { + set_time_base(Rational::from_string(reader->read_element_text())); + } else if (name == "format") { + set_format(static_cast( + std::stoi(reader->read_element_text()))); + } else if (name == "channelcount") { + set_channel_count(std::stoi(reader->read_element_text())); + } else if (name == "pixelaspectratio") { + set_pixel_aspect_ratio( + Rational::from_string(reader->read_element_text())); + } else if (name == "interlacing") { + set_interlacing(static_cast( + std::stoi(reader->read_element_text()))); + } else if (name == "divider") { + set_divider(std::stoi(reader->read_element_text())); + } else if (name == "enabled") { + set_enabled(std::stoi(reader->read_element_text())); + } else if (name == "x") { + set_x(std::stof(reader->read_element_text())); + } else if (name == "y") { + set_y(std::stof(reader->read_element_text())); + } else if (name == "streamindex") { + set_stream_index(std::stoi(reader->read_element_text())); + } else if (name == "videotype") { + set_video_type(static_cast( + std::stoi(reader->read_element_text()))); + } else if (name == "framerate") { + set_frame_rate(Rational::from_string(reader->read_element_text())); + } else if (name == "starttime") { + set_start_time(std::stoll(reader->read_element_text())); + } else if (name == "duration") { + set_duration(std::stoll(reader->read_element_text())); + } else if (name == "premultipliedalpha") { + set_premultiplied_alpha(std::stoi(reader->read_element_text())); + } else if (name == "colorspace") { + set_colorspace(reader->read_element_text()); + } else if (name == "colorrange") { + set_color_range( + static_cast(std::stoi(reader->read_element_text()))); + } else if (name == "colorprimaries") { + set_color_primaries(std::stoi(reader->read_element_text())); + } else if (name == "colortransfer") { + set_color_transfer(std::stoi(reader->read_element_text())); + } else { + reader->skip_current_element(); + } + } +} + +void VideoParams::save(XmlStreamWriter *writer) const +{ + writer->write_text_element("width", std::to_string(width_)); + writer->write_text_element("height", std::to_string(height_)); + writer->write_text_element("depth", std::to_string(depth_)); + writer->write_text_element("timebase", time_base_.to_string()); + writer->write_text_element("format", std::to_string(format_)); + writer->write_text_element("channelcount", std::to_string(channel_count_)); + writer->write_text_element("pixelaspectratio", + pixel_aspect_ratio_.to_string()); + writer->write_text_element("interlacing", std::to_string(interlacing_)); + writer->write_text_element("divider", std::to_string(divider_)); + writer->write_text_element("enabled", std::to_string(enabled_)); + writer->write_text_element("x", std::to_string(x_)); + writer->write_text_element("y", std::to_string(y_)); + writer->write_text_element("streamindex", std::to_string(stream_index_)); + writer->write_text_element("videotype", std::to_string(video_type_)); + writer->write_text_element("framerate", frame_rate_.to_string()); + writer->write_text_element("starttime", std::to_string(start_time_)); + writer->write_text_element("duration", std::to_string(duration_)); + writer->write_text_element("premultipliedalpha", + std::to_string(premultiplied_alpha_)); + writer->write_text_element("colorspace", colorspace_); + writer->write_text_element("colorrange", std::to_string(color_range_)); + writer->write_text_element("colorprimaries", std::to_string(color_primaries_)); + writer->write_text_element("colortransfer", std::to_string(color_transfer_)); +} + +} diff --git a/src/common/src/videoparams.h b/src/common/src/videoparams.h new file mode 100644 index 000000000..399bbac59 --- /dev/null +++ b/src/common/src/videoparams.h @@ -0,0 +1,458 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_VIDEOPARAMS_H +#define OAK_VIDEOPARAMS_H + +#include +#include +#include + +#include +#include + +#include "xmlutils.h" + +namespace olive +{ + +using namespace core; + +/** + * @brief Pure-data video parameter set (sunk from engine/render/videoparams.h). + * + * De-Qt port: QString -> std::string, QVector -> std::vector, + * QXmlStreamReader/Writer -> olive::XmlStreamReader/Writer. The QVector2D + * convenience getters (resolution/square_resolution/offset) were dropped; + * use width()/height()/x()/y() directly. Translated UI strings are returned + * as plain English text. + */ +class VideoParams { +public: + enum Interlacing { + k_interlace_none, + k_interlaced_top_first, + k_interlaced_bottom_first + }; + + enum Type { k_video_type_video, k_video_type_still, k_video_type_image_sequence }; + enum ColorRange { + k_color_range_limited, // 16-235 + k_color_range_full, // 0-255 + + k_color_range_default = k_color_range_limited + }; + + VideoParams(); + VideoParams(int width, int height, PixelFormat format, int nb_channels, + const Rational &pixel_aspect_ratio = 1, + Interlacing interlacing = k_interlace_none, int divider = 1); + VideoParams(int width, int height, int depth, PixelFormat format, + int nb_channels, const Rational &pixel_aspect_ratio = 1, + Interlacing interlacing = k_interlace_none, int divider = 1); + VideoParams(int width, int height, const Rational &time_base, + PixelFormat format, int nb_channels, + const Rational &pixel_aspect_ratio = 1, + Interlacing interlacing = k_interlace_none, int divider = 1); + + int width() const + { + return width_; + } + + void set_width(int width) + { + width_ = width; + calculate_effective_size(); + } + + /** + * @brief Returns width multiplied by pixel aspect ratio where applicable + */ + int square_pixel_width() const + { + return par_width_; + } + + int height() const + { + return height_; + } + + void set_height(int height) + { + height_ = height; + calculate_effective_size(); + } + + int depth() const + { + return depth_; + } + + void set_depth(int depth) + { + depth_ = depth; + calculate_effective_size(); + } + + bool is_3d() const + { + return depth_ > 1; + } + + const Rational &time_base() const + { + return time_base_; + } + + void set_time_base(const Rational &r) + { + time_base_ = r; + } + + Rational frame_rate_as_time_base() const + { + return frame_rate_.flipped(); + } + + int divider() const + { + return divider_; + } + + void set_divider(int d) + { + divider_ = d; + calculate_effective_size(); + } + + int effective_width() const + { + return effective_width_; + } + + int effective_height() const + { + return effective_height_; + } + + int effective_depth() const + { + return effective_depth_; + } + + PixelFormat format() const + { + return format_; + } + + void set_format(PixelFormat f) + { + format_ = f; + } + + int channel_count() const + { + return channel_count_; + } + + void set_channel_count(int c) + { + channel_count_ = c; + } + void set_channel_count(const std::string &ofx_component); + const Rational &pixel_aspect_ratio() const + { + return pixel_aspect_ratio_; + } + + void set_pixel_aspect_ratio(const Rational &r) + { + pixel_aspect_ratio_ = r; + validate_pixel_aspect_ratio(); + } + + Interlacing interlacing() const + { + return interlacing_; + } + + void set_interlacing(Interlacing i) + { + interlacing_ = i; + } + + static int generate_auto_divider(int64_t width, int64_t height); + + bool is_valid() const; + + bool operator==(const VideoParams &rhs) const; + bool operator!=(const VideoParams &rhs) const; + + static int get_bytes_per_channel(PixelFormat format); + int get_bytes_per_channel() const + { + return get_bytes_per_channel(format_); + } + + static int get_bytes_per_pixel(PixelFormat format, int channels); + int get_bytes_per_pixel() const + { + return get_bytes_per_pixel(format_, channel_count_); + } + + static int get_buffer_size(int width, int height, PixelFormat format, + int channels) + { + return width * height * get_bytes_per_pixel(format, channels); + } + int get_buffer_size() const + { + return get_buffer_size(width_, height_, format_, channel_count_); + } + + static std::string get_name_for_divider(int div); + + static bool format_is_float(PixelFormat format) + { + return format.is_float(); + } + + static std::string get_format_name(PixelFormat format); + + static int get_divider_for_target_resolution(int src_width, int src_height, + int dst_width, int dst_height); + + static const int k_internal_channel_count; + + static const Rational k_pixel_aspect_square; + static const Rational k_pixel_aspect_ntsc_standard; + static const Rational k_pixel_aspect_ntsc_widescreen; + static const Rational k_pixel_aspect_pal_standard; + static const Rational k_pixel_aspect_pal_widescreen; + static const Rational k_pixel_aspect1080_anamorphic; + + static const std::vector k_supported_frame_rates; + static const std::vector k_standard_pixel_aspects; + static const std::vector k_supported_dividers; + + static const int k_hsv_channel_count = 3; + static const int k_rgb_channel_count = 3; + static const int k_rgba_channel_count = 4; + + /** + * @brief Convert Rational frame rate (i.e. flipped timebase) to a user-friendly string + */ + static std::string frame_rate_to_string(const Rational &frame_rate); + + static std::vector get_standard_pixel_aspect_ratio_names(); + static std::string format_pixel_aspect_ratio_string(const std::string &format, + const Rational &ratio); + + static int get_scaled_dimension(int dim, int divider); + + bool enabled() const + { + return enabled_; + } + + void set_enabled(bool e) + { + enabled_ = e; + } + + float x() const + { + return x_; + } + void set_x(float x) + { + x_ = x; + } + float y() const + { + return y_; + } + void set_y(float y) + { + y_ = y; + } + + int stream_index() const + { + return stream_index_; + } + + void set_stream_index(int s) + { + stream_index_ = s; + } + + Type video_type() const + { + return video_type_; + } + + void set_video_type(Type t) + { + video_type_ = t; + } + + const Rational &frame_rate() const + { + return frame_rate_; + } + + void set_frame_rate(const Rational &frame_rate) + { + frame_rate_ = frame_rate; + } + + int64_t start_time() const + { + return start_time_; + } + + void set_start_time(int64_t start_time) + { + start_time_ = start_time; + } + + int64_t duration() const + { + return duration_; + } + + void set_duration(int64_t duration) + { + duration_ = duration; + } + + bool premultiplied_alpha() const + { + return premultiplied_alpha_; + } + + void set_premultiplied_alpha(bool premultiplied_alpha) + { + premultiplied_alpha_ = premultiplied_alpha; + } + + const std::string &colorspace() const + { + return colorspace_; + } + + void set_colorspace(const std::string &c) + { + colorspace_ = c; + } + + const ColorRange &color_range() const + { + return color_range_; + } + void set_color_range(const ColorRange &color_range) + { + color_range_ = color_range; + } + + /** + * @brief Color primaries/transfer as reported by the media + * + * Raw FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values + * (0 = unset, 2 = unspecified). Used to auto-detect the input + * colorspace when no explicit colorspace has been set. + */ + int color_primaries() const + { + return color_primaries_; + } + + void set_color_primaries(int p) + { + color_primaries_ = p; + } + + int color_transfer() const + { + return color_transfer_; + } + + void set_color_transfer(int t) + { + color_transfer_ = t; + } + + int64_t get_time_in_timebase_units(const Rational &time) const; + + void load(XmlStreamReader *reader); + + void save(XmlStreamWriter *writer) const; + +private: + void calculate_effective_size(); + + void validate_pixel_aspect_ratio(); + + void set_defaults_for_footage(); + + void calculate_square_pixel_width(); + + int width_; + int height_; + int depth_; + Rational time_base_; + + PixelFormat format_; + + int channel_count_; + + Rational pixel_aspect_ratio_; + + Interlacing interlacing_; + + int divider_; + + // Cached values + int effective_width_; + int effective_height_; + int effective_depth_; + int par_width_; + + bool enabled_; + int stream_index_; + Type video_type_; + Rational frame_rate_; + int64_t start_time_; + int64_t duration_; + bool premultiplied_alpha_; + std::string colorspace_; + float x_; + float y_; + ColorRange color_range_; + int color_primaries_ = 0; + int color_transfer_ = 0; +}; + +} + +#endif // OAK_VIDEOPARAMS_H diff --git a/src/common/tests/CMakeLists.txt b/src/common/tests/CMakeLists.txt index 6c2e65719..aa9b9d561 100644 --- a/src/common/tests/CMakeLists.txt +++ b/src/common/tests/CMakeLists.txt @@ -19,6 +19,7 @@ find_package(GTest REQUIRED) include(GoogleTest) add_executable(oakcommon-gtest + colortransform_test.cpp commandlineparser_test.cpp current_test.cpp debug_test.cpp @@ -31,7 +32,9 @@ add_executable(oakcommon-gtest oiioutils_test.cpp power_test.cpp qtutils_test.cpp + subtitleparams_test.cpp threadsafemap_test.cpp + videoparams_test.cpp xmlutils_test.cpp ) diff --git a/src/common/tests/colortransform_test.cpp b/src/common/tests/colortransform_test.cpp new file mode 100644 index 000000000..4923cdce4 --- /dev/null +++ b/src/common/tests/colortransform_test.cpp @@ -0,0 +1,121 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "common/colortransform.h" + +namespace +{ + +std::string read_string(int (*fn)(OakCommonColorTransform *, char *, int), + OakCommonColorTransform *t) +{ + int needed = fn(t, nullptr, 0); + EXPECT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(fn(t, buf.data(), needed), needed); + return std::string(buf.data()); +} + +} // namespace + +TEST(CommonColorTransformCApi, InitOutput) +{ + OakCommonColorTransform *t = + oakcommon_colortransform_init_output("sRGB"); + ASSERT_NE(t, nullptr); + + int is_display = -1; + EXPECT_EQ(oakcommon_colortransform_is_display(t, &is_display), + OAKCOMMON_OK); + EXPECT_EQ(is_display, 0); + EXPECT_EQ(read_string(oakcommon_colortransform_get_output, t), "sRGB"); + EXPECT_EQ(read_string(oakcommon_colortransform_get_display, t), "sRGB"); + + oakcommon_colortransform_free(t); +} + +TEST(CommonColorTransformCApi, InitOutputNullString) +{ + EXPECT_EQ(oakcommon_colortransform_init_output(nullptr), nullptr); +} + +TEST(CommonColorTransformCApi, InitDisplay) +{ + OakCommonColorTransform *t = + oakcommon_colortransform_init_display("sRGB", "Studio", "None"); + ASSERT_NE(t, nullptr); + + int is_display = 0; + EXPECT_EQ(oakcommon_colortransform_is_display(t, &is_display), + OAKCOMMON_OK); + EXPECT_EQ(is_display, 1); + EXPECT_EQ(read_string(oakcommon_colortransform_get_display, t), "sRGB"); + EXPECT_EQ(read_string(oakcommon_colortransform_get_view, t), "Studio"); + EXPECT_EQ(read_string(oakcommon_colortransform_get_look, t), "None"); + + oakcommon_colortransform_free(t); +} + +TEST(CommonColorTransformCApi, InitDisplayNullString) +{ + EXPECT_EQ(oakcommon_colortransform_init_display(nullptr, "v", "l"), + nullptr); + EXPECT_EQ(oakcommon_colortransform_init_display("d", nullptr, "l"), + nullptr); + EXPECT_EQ(oakcommon_colortransform_init_display("d", "v", nullptr), + nullptr); +} + +TEST(CommonColorTransformCApi, FreeNull) +{ + oakcommon_colortransform_free(nullptr); +} + +TEST(CommonColorTransformCApi, NullHandleErrors) +{ + int i = 0; + char buf[16]; + EXPECT_EQ(oakcommon_colortransform_is_display(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_colortransform_get_display(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_colortransform_get_output(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_colortransform_get_view(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_colortransform_get_look(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); +} + +TEST(CommonColorTransformCApi, NullOutParam) +{ + OakCommonColorTransform *t = + oakcommon_colortransform_init_output("sRGB"); + ASSERT_NE(t, nullptr); + EXPECT_EQ(oakcommon_colortransform_is_display(t, nullptr), + OAKCOMMON_E_INVALID); + oakcommon_colortransform_free(t); +} diff --git a/src/common/tests/subtitleparams_test.cpp b/src/common/tests/subtitleparams_test.cpp new file mode 100644 index 000000000..ca2e8711e --- /dev/null +++ b/src/common/tests/subtitleparams_test.cpp @@ -0,0 +1,251 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "common/subtitleparams.h" + +namespace +{ + +std::string read_indexed_string( + int (*fn)(OakCommonSubtitleParams *, int, char *, int), + OakCommonSubtitleParams *p, int index) +{ + int needed = fn(p, index, nullptr, 0); + EXPECT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(fn(p, index, buf.data(), needed), needed); + return std::string(buf.data()); +} + +std::string read_static_string(int (*fn)(char *, int)) +{ + int needed = fn(nullptr, 0); + EXPECT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(fn(buf.data(), needed), needed); + return std::string(buf.data()); +} + +} // namespace + +TEST(CommonSubtitleParamsCApi, InitFree) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + + int index = -1; + EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(p, &index), + OAKCOMMON_OK); + EXPECT_EQ(index, 0); + + int enabled = 0; + EXPECT_EQ(oakcommon_subtitleparams_get_enabled(p, &enabled), + OAKCOMMON_OK); + EXPECT_EQ(enabled, 1); + + oakcommon_subtitleparams_free(p); +} + +TEST(CommonSubtitleParamsCApi, FreeNull) +{ + oakcommon_subtitleparams_free(nullptr); +} + +TEST(CommonSubtitleParamsCApi, SetStreamIndex) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(p, 3), OAKCOMMON_OK); + int index = 0; + EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(p, &index), + OAKCOMMON_OK); + EXPECT_EQ(index, 3); + oakcommon_subtitleparams_free(p); +} + +TEST(CommonSubtitleParamsCApi, SetStreamIndexNullHandle) +{ + EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(nullptr, 3), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(nullptr, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(CommonSubtitleParamsCApi, SetEnabled) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_subtitleparams_set_enabled(p, 0), OAKCOMMON_OK); + int enabled = 1; + EXPECT_EQ(oakcommon_subtitleparams_get_enabled(p, &enabled), + OAKCOMMON_OK); + EXPECT_EQ(enabled, 0); + oakcommon_subtitleparams_free(p); +} + +TEST(CommonSubtitleParamsCApi, SetEnabledNullHandle) +{ + EXPECT_EQ(oakcommon_subtitleparams_set_enabled(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_get_enabled(nullptr, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(CommonSubtitleParamsCApi, AddAndReadSubtitles) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + + int is_valid = 1; + EXPECT_EQ(oakcommon_subtitleparams_is_valid(p, &is_valid), OAKCOMMON_OK); + EXPECT_EQ(is_valid, 0); + + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 1, 2, 3, 2, "Hello"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 2, 1, 3, 1, "World"), + OAKCOMMON_OK); + + EXPECT_EQ(oakcommon_subtitleparams_is_valid(p, &is_valid), OAKCOMMON_OK); + EXPECT_EQ(is_valid, 1); + + int count = 0; + EXPECT_EQ(oakcommon_subtitleparams_count(p, &count), OAKCOMMON_OK); + EXPECT_EQ(count, 2); + + int in_num, in_den, out_num, out_den; + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(p, 0, &in_num, &in_den, + &out_num, &out_den), + OAKCOMMON_OK); + EXPECT_EQ(in_num, 1); + EXPECT_EQ(in_den, 2); + EXPECT_EQ(out_num, 3); + EXPECT_EQ(out_den, 2); + EXPECT_EQ(read_indexed_string( + oakcommon_subtitleparams_get_subtitle_text, p, 0), + "Hello"); + + int num, den; + EXPECT_EQ(oakcommon_subtitleparams_duration(p, &num, &den), OAKCOMMON_OK); + EXPECT_EQ(num, 3); + EXPECT_EQ(den, 1); + + EXPECT_EQ(oakcommon_subtitleparams_clear(p), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_subtitleparams_count(p, &count), OAKCOMMON_OK); + EXPECT_EQ(count, 0); + + oakcommon_subtitleparams_free(p); +} + +TEST(CommonSubtitleParamsCApi, SubtitleErrorPaths) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + + int i = 0; + char buf[16]; + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(nullptr, 0, 1, 1, 1, "x"), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 0, 1, 1, 1, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(p, 0, &i, &i, &i, &i), + OAKCOMMON_E_NOT_FOUND); + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(nullptr, 0, &i, &i, &i, + &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle_text(p, 5, buf, + sizeof(buf)), + OAKCOMMON_E_NOT_FOUND); + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle_text(nullptr, 0, buf, + sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_is_valid(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_count(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_duration(nullptr, &i, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_clear(nullptr), OAKCOMMON_E_INVALID); + + oakcommon_subtitleparams_free(p); +} + +TEST(CommonSubtitleParamsCApi, GenerateAssHeader) +{ + std::string header = + read_static_string(oakcommon_subtitleparams_generate_ass_header); + EXPECT_NE(header.find("[Script Info]"), std::string::npos); + EXPECT_NE(header.find("[V4+ Styles]"), std::string::npos); + EXPECT_NE(header.find("[Events]"), std::string::npos); +} + +TEST(CommonSubtitleParamsCApi, XmlRoundTrip) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(p, 2), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 1, 25, 2, 25, + "First "), + OAKCOMMON_OK); + + int needed = oakcommon_subtitleparams_save_xml(p, nullptr, 0); + ASSERT_GT(needed, 0); + std::vector buf(needed); + ASSERT_EQ(oakcommon_subtitleparams_save_xml(p, buf.data(), needed), needed); + + OakCommonSubtitleParams *q = oakcommon_subtitleparams_init(); + ASSERT_NE(q, nullptr); + ASSERT_EQ(oakcommon_subtitleparams_load_xml(q, buf.data()), OAKCOMMON_OK); + + int index = 0; + EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(q, &index), + OAKCOMMON_OK); + EXPECT_EQ(index, 2); + int count = 0; + EXPECT_EQ(oakcommon_subtitleparams_count(q, &count), OAKCOMMON_OK); + EXPECT_EQ(count, 1); + EXPECT_EQ(read_indexed_string( + oakcommon_subtitleparams_get_subtitle_text, q, 0), + "First "); + + oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(q); +} + +TEST(CommonSubtitleParamsCApi, XmlErrorPaths) +{ + OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); + ASSERT_NE(p, nullptr); + char buf[16]; + EXPECT_EQ(oakcommon_subtitleparams_load_xml(nullptr, ""), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_load_xml(p, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_load_xml(p, "not xml"), + OAKCOMMON_E_FAILED); + EXPECT_EQ(oakcommon_subtitleparams_save_xml(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + oakcommon_subtitleparams_free(p); +} diff --git a/src/common/tests/videoparams_test.cpp b/src/common/tests/videoparams_test.cpp new file mode 100644 index 000000000..2d3bf20d8 --- /dev/null +++ b/src/common/tests/videoparams_test.cpp @@ -0,0 +1,525 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include +#include + +#include "common/videoparams.h" + +TEST(CommonVideoParamsCApi, InitDefaults) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init(); + ASSERT_NE(p, nullptr); + + int v = -1; + EXPECT_EQ(oakcommon_videoparams_get_width(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 0); + EXPECT_EQ(oakcommon_videoparams_get_is_valid(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 0); + + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, FreeNull) +{ + oakcommon_videoparams_free(nullptr); +} + +TEST(CommonVideoParamsCApi, InitBasic) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init_basic( + 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, + 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 2); + ASSERT_NE(p, nullptr); + + int v; + EXPECT_EQ(oakcommon_videoparams_get_width(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1920); + EXPECT_EQ(oakcommon_videoparams_get_height(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1080); + EXPECT_EQ(oakcommon_videoparams_get_depth(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1); + EXPECT_EQ(oakcommon_videoparams_get_is_3d(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 0); + EXPECT_EQ(oakcommon_videoparams_get_format(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, OAKCOMMON_PIXEL_FORMAT_U8); + EXPECT_EQ(oakcommon_videoparams_get_channel_count(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 4); + EXPECT_EQ(oakcommon_videoparams_get_divider(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 2); + EXPECT_EQ(oakcommon_videoparams_get_interlacing(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, OAKCOMMON_VIDEO_INTERLACE_NONE); + EXPECT_EQ(oakcommon_videoparams_get_is_valid(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1); + + // Effective size is divided by the divider + EXPECT_EQ(oakcommon_videoparams_get_effective_width(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 960); + EXPECT_EQ(oakcommon_videoparams_get_effective_height(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 540); + EXPECT_EQ(oakcommon_videoparams_get_square_pixel_width(p, &v), + OAKCOMMON_OK); + EXPECT_EQ(v, 1920); + + // u8 RGBA: 4 bytes per pixel + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel(p, &v), + OAKCOMMON_OK); + EXPECT_EQ(v, 1); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 4); + EXPECT_EQ(oakcommon_videoparams_get_buffer_size(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1920 * 1080 * 4); + + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, InitWithTimeBase) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + 1280, 720, 1001, 30000, OAKCOMMON_PIXEL_FORMAT_F32, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + ASSERT_NE(p, nullptr); + + int num, den; + EXPECT_EQ(oakcommon_videoparams_get_time_base(p, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 1001); + EXPECT_EQ(den, 30000); + + // Frame rate is the flipped time base + EXPECT_EQ(oakcommon_videoparams_get_frame_rate(p, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 30000); + EXPECT_EQ(den, 1001); + EXPECT_EQ(oakcommon_videoparams_frame_rate_as_time_base(p, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 1001); + EXPECT_EQ(den, 30000); + + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, ScalarSetters) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init(); + ASSERT_NE(p, nullptr); + + int v; + EXPECT_EQ(oakcommon_videoparams_set_width(p, 640), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_height(p, 480), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_depth(p, 4), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_get_is_3d(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1); + EXPECT_EQ(oakcommon_videoparams_set_format(p, OAKCOMMON_PIXEL_FORMAT_U16), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_channel_count(p, 3), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_interlacing( + p, OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_divider(p, 4), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_enabled(p, 0), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_stream_index(p, 5), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_video_type( + p, OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_premultiplied_alpha(p, 1), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_color_range( + p, OAKCOMMON_COLOR_RANGE_FULL), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_color_primaries(p, 9), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_color_transfer(p, 16), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_time_base(p, 1, 25), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_frame_rate(p, 25, 1), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_pixel_aspect_ratio(p, 4, 3), + OAKCOMMON_OK); + + float f; + EXPECT_EQ(oakcommon_videoparams_set_x(p, 1.5f), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_y(p, -2.5f), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_get_x(p, &f), OAKCOMMON_OK); + EXPECT_FLOAT_EQ(f, 1.5f); + EXPECT_EQ(oakcommon_videoparams_get_y(p, &f), OAKCOMMON_OK); + EXPECT_FLOAT_EQ(f, -2.5f); + + int64_t i64; + EXPECT_EQ(oakcommon_videoparams_set_start_time(p, 100), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_duration(p, 250), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_get_start_time(p, &i64), OAKCOMMON_OK); + EXPECT_EQ(i64, 100); + EXPECT_EQ(oakcommon_videoparams_get_duration(p, &i64), OAKCOMMON_OK); + EXPECT_EQ(i64, 250); + + EXPECT_EQ(oakcommon_videoparams_get_width(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 640); + EXPECT_EQ(oakcommon_videoparams_get_effective_width(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 160); + EXPECT_EQ(oakcommon_videoparams_get_interlacing(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST); + EXPECT_EQ(oakcommon_videoparams_get_enabled(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 0); + EXPECT_EQ(oakcommon_videoparams_get_stream_index(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 5); + EXPECT_EQ(oakcommon_videoparams_get_video_type(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE); + EXPECT_EQ(oakcommon_videoparams_get_premultiplied_alpha(p, &v), + OAKCOMMON_OK); + EXPECT_EQ(v, 1); + EXPECT_EQ(oakcommon_videoparams_get_color_range(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, OAKCOMMON_COLOR_RANGE_FULL); + EXPECT_EQ(oakcommon_videoparams_get_color_primaries(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 9); + EXPECT_EQ(oakcommon_videoparams_get_color_transfer(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 16); + + int num, den; + EXPECT_EQ(oakcommon_videoparams_get_pixel_aspect_ratio(p, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 4); + EXPECT_EQ(den, 3); + // Square pixel width = 640 * 4/3 = 853.33 -> 853 + EXPECT_EQ(oakcommon_videoparams_get_square_pixel_width(p, &v), + OAKCOMMON_OK); + EXPECT_EQ(v, 853); + EXPECT_EQ(oakcommon_videoparams_get_effective_depth(p, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 1); + + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, NullHandleErrors) +{ + int i; + float f; + int64_t i64; + char buf[16]; + EXPECT_EQ(oakcommon_videoparams_get_width(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_width(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_height(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_height(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_depth(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_depth(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_is_3d(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_time_base(nullptr, &i, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_time_base(nullptr, 1, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_frame_rate(nullptr, &i, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_frame_rate(nullptr, 1, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_frame_rate_as_time_base(nullptr, &i, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_pixel_aspect_ratio(nullptr, &i, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_pixel_aspect_ratio(nullptr, 1, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_format(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_format(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_channel_count(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_channel_count(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_interlacing(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_interlacing(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_divider(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_divider(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_enabled(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_enabled(nullptr, 1), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_x(nullptr, &f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_x(nullptr, 0.0f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_y(nullptr, &f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_y(nullptr, 0.0f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_stream_index(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_stream_index(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_video_type(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_video_type(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_start_time(nullptr, &i64), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_start_time(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_duration(nullptr, &i64), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_duration(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_premultiplied_alpha(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_premultiplied_alpha(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_color_range(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_color_range(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_color_primaries(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_color_primaries(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_color_transfer(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_color_transfer(nullptr, 0), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_colorspace(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_colorspace(nullptr, "x"), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_square_pixel_width(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_effective_width(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_effective_height(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_effective_depth(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_is_valid(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_buffer_size(nullptr, &i), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(nullptr, 0, 1, + &i64), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_load_xml(nullptr, ""), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_save_xml(nullptr, buf, sizeof(buf)), + OAKCOMMON_E_INVALID); +} + +TEST(CommonVideoParamsCApi, NullOutParamErrors) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init(); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_videoparams_get_width(p, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_time_base(p, nullptr, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_colorspace(p, nullptr), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_load_xml(p, nullptr), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_load_xml(p, "not xml"), + OAKCOMMON_E_FAILED); + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, Colorspace) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init(); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_videoparams_set_colorspace(p, "rec709"), + OAKCOMMON_OK); + int needed = oakcommon_videoparams_get_colorspace(p, nullptr, 0); + ASSERT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(oakcommon_videoparams_get_colorspace(p, buf.data(), needed), + needed); + EXPECT_STREQ(buf.data(), "rec709"); + oakcommon_videoparams_free(p); +} + +TEST(CommonVideoParamsCApi, TimeInTimebaseUnits) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + 1920, 1080, 1, 25, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + ASSERT_NE(p, nullptr); + + int64_t ts = -1; + EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(p, 2, 1, &ts), + OAKCOMMON_OK); + EXPECT_EQ(ts, 50); // 2 seconds at 25 fps + + // Without a time base the result is AV_NOPTS_VALUE + OakCommonVideoParams *q = oakcommon_videoparams_init(); + ASSERT_NE(q, nullptr); + EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(q, 2, 1, &ts), + OAKCOMMON_OK); + EXPECT_EQ(ts, INT64_MIN); + + oakcommon_videoparams_free(p); + oakcommon_videoparams_free(q); +} + +TEST(CommonVideoParamsCApi, Equals) +{ + OakCommonVideoParams *a = oakcommon_videoparams_init_basic( + 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + OakCommonVideoParams *b = oakcommon_videoparams_init_basic( + 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + ASSERT_NE(a, nullptr); + ASSERT_NE(b, nullptr); + + int equal = 0; + EXPECT_EQ(oakcommon_videoparams_equals(a, b, &equal), OAKCOMMON_OK); + EXPECT_EQ(equal, 1); + + EXPECT_EQ(oakcommon_videoparams_set_divider(b, 2), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_equals(a, b, &equal), OAKCOMMON_OK); + EXPECT_EQ(equal, 0); + + EXPECT_EQ(oakcommon_videoparams_equals(nullptr, b, &equal), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_equals(a, nullptr, &equal), + OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_equals(a, b, nullptr), + OAKCOMMON_E_INVALID); + + oakcommon_videoparams_free(a); + oakcommon_videoparams_free(b); +} + +TEST(CommonVideoParamsCApi, XmlRoundTrip) +{ + OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + 1920, 1080, 1, 25, OAKCOMMON_PIXEL_FORMAT_F16, 4, 4, 3, + OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST, 2); + ASSERT_NE(p, nullptr); + EXPECT_EQ(oakcommon_videoparams_set_colorspace(p, "rec709"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_videoparams_set_color_primaries(p, 9), OAKCOMMON_OK); + + int needed = oakcommon_videoparams_save_xml(p, nullptr, 0); + ASSERT_GT(needed, 0); + std::vector buf(needed); + ASSERT_EQ(oakcommon_videoparams_save_xml(p, buf.data(), needed), needed); + + OakCommonVideoParams *q = oakcommon_videoparams_init(); + ASSERT_NE(q, nullptr); + ASSERT_EQ(oakcommon_videoparams_load_xml(q, buf.data()), OAKCOMMON_OK); + + int equal = 0; + EXPECT_EQ(oakcommon_videoparams_equals(p, q, &equal), OAKCOMMON_OK); + EXPECT_EQ(equal, 1); + + int num, den; + EXPECT_EQ(oakcommon_videoparams_get_frame_rate(q, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(num, 25); + EXPECT_EQ(den, 1); + + int cneeded = oakcommon_videoparams_get_colorspace(q, nullptr, 0); + std::vector cbuf(cneeded); + EXPECT_EQ(oakcommon_videoparams_get_colorspace(q, cbuf.data(), cneeded), + cneeded); + EXPECT_STREQ(cbuf.data(), "rec709"); + + int v; + EXPECT_EQ(oakcommon_videoparams_get_color_primaries(q, &v), OAKCOMMON_OK); + EXPECT_EQ(v, 9); + + oakcommon_videoparams_free(p); + oakcommon_videoparams_free(q); +} + +TEST(CommonVideoParamsCApi, StaticHelpers) +{ + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel_for_format( + OAKCOMMON_PIXEL_FORMAT_U8), + 1); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel_for_format( + OAKCOMMON_PIXEL_FORMAT_F32), + 4); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel_for_format( + OAKCOMMON_PIXEL_FORMAT_INVALID), + 0); + + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel_for_format( + OAKCOMMON_PIXEL_FORMAT_U10, 4), + 4); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel_for_format( + OAKCOMMON_PIXEL_FORMAT_U10, 3), + 0); + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel_for_format( + OAKCOMMON_PIXEL_FORMAT_U16, 3), + 6); + + EXPECT_EQ(oakcommon_videoparams_calculate_buffer_size( + 100, 50, OAKCOMMON_PIXEL_FORMAT_U8, 4), + 20000); + + EXPECT_EQ(oakcommon_videoparams_format_is_float( + OAKCOMMON_PIXEL_FORMAT_F16), + 1); + EXPECT_EQ(oakcommon_videoparams_format_is_float(OAKCOMMON_PIXEL_FORMAT_U8), + 0); + + EXPECT_EQ(oakcommon_videoparams_generate_auto_divider(1920, 1080), 2); + EXPECT_EQ(oakcommon_videoparams_generate_auto_divider(640, 480), 1); + EXPECT_EQ(oakcommon_videoparams_generate_auto_divider(16000, 16000), 16); + + EXPECT_EQ(oakcommon_videoparams_get_scaled_dimension(1920, 2), 960); + EXPECT_EQ(oakcommon_videoparams_get_divider_for_target_resolution( + 3840, 2160, 1920, 1080), + 2); +} + +TEST(CommonVideoParamsCApi, StaticStrings) +{ + char buf[64]; + + int needed = oakcommon_videoparams_get_name_for_divider(1, nullptr, 0); + ASSERT_GT(needed, 0); + EXPECT_EQ(oakcommon_videoparams_get_name_for_divider(1, buf, sizeof(buf)), + needed); + EXPECT_STREQ(buf, "Full"); + EXPECT_GT(oakcommon_videoparams_get_name_for_divider(4, buf, sizeof(buf)), + 0); + EXPECT_STREQ(buf, "1/4"); + + EXPECT_GT(oakcommon_videoparams_get_format_name(OAKCOMMON_PIXEL_FORMAT_U8, + buf, sizeof(buf)), + 0); + EXPECT_STREQ(buf, "8-bit"); + EXPECT_GT(oakcommon_videoparams_get_format_name( + OAKCOMMON_PIXEL_FORMAT_INVALID, buf, sizeof(buf)), + 0); + EXPECT_NE(std::string(buf).find("Unknown"), std::string::npos); + + EXPECT_GT(oakcommon_videoparams_frame_rate_to_string(25, 1, buf, + sizeof(buf)), + 0); + EXPECT_STREQ(buf, "25 FPS"); +} diff --git a/src/node/CMakeLists.txt b/src/node/CMakeLists.txt new file mode 100644 index 000000000..f41570121 --- /dev/null +++ b/src/node/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(src) +add_subdirectory(c_api) + +if(BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/src/node/DEQT.md b/src/node/DEQT.md new file mode 100644 index 000000000..f5b1b5447 --- /dev/null +++ b/src/node/DEQT.md @@ -0,0 +1,313 @@ +# oaknode 去 Qt 化约定(DEQT) + +> 第一波(核心基建)确立的替换规则。后续波次处理叶子节点文件时**严格照此机械替换,不改任何行为逻辑**。 +> 有疑问先查本文件;本文件没覆盖的 Qt 类型,在 `src/node/DEQT.md` 补一条规则再动手。 + +## 1. 替换映射表 + +| Qt | 替代 | 说明 | +|---|---|---| +| `QString` | `std::string` | 默认参数 `QString()` → `std::string()`;`isEmpty()` → `empty()`;`==/!=` 直接用 | +| `QStringList` | `olive::StringList`(= `std::vector`,定义在 `node/variant.h`) | | +| `QByteArray` | `olive::ByteArray`(= `std::vector`) | base64 用 `olive::byte_array_to_base64()` / `byte_array_from_base64()` | +| `QVector` | `std::vector` | `append`→`push_back`,`prepend`→`insert(begin(), x)`,`takeAt(i)`→取值后 `erase(begin()+i)`,`removeAt`→`erase`,`contains`→`std::find(...)!=end()`,`indexOf`→`std::find`-`begin()`(无则 -1),`first/last`→`front/back`,`count/size`→`size()`(必要时 `int(...)`),`isEmpty`→`empty` | +| `QList` | `std::vector` | 同上 | +| `QStringList::split` / `s.split(':')` | `olive::core::StringUtils::split(s, ':')`(`olive/core/util/stringutils.h`) | | +| `QStringLiteral("x")` | `"x"` | | +| `QString::number(x)` | `std::to_string(x)`(整数);double/float 用 `snprintf("%g")`(Qt 默认 'g' 6 位有效数字,value.cpp 里有 `number_to_string()` 静态函数可复制) | | +| `QString("%1...").arg(a,b)` | 字符串拼接 | 保持参数顺序与原文一致 | +| `s.toStdString()` | 直接用(已是 std::string) | | +| `s.toFloat()/toDouble()/toLongLong()` | `strtof/strtod/strtoll(s.c_str(), nullptr, 10)` | 失败返回 0 的语义一致 | +| `QVariant` | `olive::Variant`(`node/variant.h`) | 见 §2 | +| `QVector2D/3D/4D` | `olive::Vector2D/3D/4D`(`node/mathtypes.h`) | float 存储,API 同名(`x()`、`set_x()` … snake_case) | +| `QMatrix4x4` | `olive::Matrix4x4`(`node/mathtypes.h`) | 行主序 `m[row][col]`,`inverted()`、`transposed()`、`operator()(r,c)` | +| `QPointF` | `olive::PointF`(`node/mathtypes.h`) | `x()/y()/set_x()/set_y()`,运算符齐全 | +| `QTransform` | `olive::Matrix4x4` | `translate(x,y)`、`scale(x,y)`、`rotate(deg)` 成员已提供(后乘语义,同 QTransform) | +| `QHash` | `std::map` 或 `std::unordered_map` | 需要键排序/迭代稳定用 `std::map`;`value(k)`→查找后返回默认值,`contains`→`count(k)` | +| `QMap` | `std::map` | | +| `QMutex` + `QMutexLocker` | `std::mutex` + `std::lock_guard` | | +| `QXmlStreamReader/Writer` | `olive::XmlStreamReader/XmlStreamWriter`(`xmlutils.h`,oakcommon) | API 形状对齐 Qt:`read_next()`→`is_start_element()` 循环;`attributes()` 返回 `std::vector`(`.name`/`.value` 都是 std::string);`readElementText()`→`read_element_text()`;`skipCurrentElement()`→`skip_current_element()`;`xml_read_next_start_element(reader)` 自由函数同 Qt 版辅助 | +| `tr("...")` / `QCoreApplication::translate("Ctx","...")` | 直接留原文字符串字面量 `"..."` | 翻译由 app 层负责 | +| `Q_OBJECT` / `signals:` / `slots:` / `emit x(...)` | 全部删除 | 见 §4 | +| `Q_DECLARE_METATYPE(T)` | 删除 | Variant 不需要注册 | +| `qHash(...)` | 删除(改用 `std::map`/`std::unordered_map`,unordered 需要时写 `std::hash` 特化) | | +| `QObject::connect/disconnect/sender()` | 删除(事件订阅移出 oaknode) | 连接信号语句整体删除,无对应逻辑保留 | +| `foreach (const T &x, list)` | `for (const T &x : list)` | | +| `qWarning() << ...` | `fprintf(stderr, ...)` | | +| `QFont` | `std::string`(family 名) | k_font 类型值直接存 family 字符串 | +| `QDateTime::fromMSecsSinceEpoch(ms, tz).toString(fmt)` | `std::tm`(`gmtime_r`/`localtime_r`)+ 按 Qt 格式 token(`yyyy/MM/dd/hh/mm/ss/zzz` 等)展开的本地静态函数 | 仅 timeformat 节点用;UTC ↔ `QTimeZone::utc()`,local ↔ `QTimeZone::systemTimeZone()` | +| `QDateTime::currentMSecsSinceEpoch()` | `std::chrono::system_clock::now()` 转毫秒 | | +| `Q_UNUSED(x)` | `(void) x;` | | +| `Q_ASSERT(x)` | `assert(x)`(``) | | +| `qMax/qMin` | `std::max/std::min`(``) | | +| `qFuzzyCompare(a,b)` | 内联展开 Qt 语义:`std::abs(a-b)*100000.0f <= std::min(std::abs(a),std::abs(b))` | | +| `qIsNull(f)` | `f == 0.0f` | | +| `Q_PROCESSOR_X86/ARM` | `OLIVE_PROCESSOR_X86/ARM` + `#include "olive/core/util/cpuoptimize.h"` | ARM 走内置 sse2neon | +| `QUuid`(render cache uuid 边界) | `std::string`:`set_uuid(text)` / `get_uuid()` 直接收发字符串 | M7 定型 cache 类时遵循 | +| `Qt::KeyboardModifiers` | `int`(gizmo_drag_move 参数) | gizmo 波次对齐 | +| `qEnvironmentVariableIsSet("X")` | `std::getenv("X") != nullptr` | | +| `quintptr` | `uintptr_t` | | +| `qAbs(x)` | `std::abs(x)`(``/``,按参数类型选头) | | +| `QPolygonF` | `std::vector` | `translate(d)` → 循环 `p += d`;`QPolygonF(QRectF(l,t,w,h))` 按 Qt 语义展开为 5 点闭包 `(l,t),(l+w,t),(l+w,t+h),(l,t+h),(l,t)` | +| `QTransform::map(QPointF)` / `QMatrix4x4::map(QPointF)` / `m.toTransform().map(p)` | `Matrix4x4::map(p)`(mathtypes.h 新增,`PointF map(const PointF&) const`) | 三者对 2D 点语义一致(z=0 透视除法);`toTransform()` 直接去掉 | +| `QVector2D::toPointF()` | `Vector2D::to_point_f()`(mathtypes.h 新增) | | +| gizmo drag 回调里的 `sender()` | `Node::current_gizmo()`(node.h 新增;返回 `NodeGizmo*`,用法 `static_cast(current_gizmo())`) | DraggableGizmo 由 gizmo 波次在直接调用回调前后 `set_current_gizmo()`;回调外为 nullptr | +| `QPainter/QBrush/QLinearGradient/QColor` 等 UI 绘制 | 删除,属 app 层 | 在 §4 清单记录 | +| `QPainterPath` | `olive::PainterPath`(`node/geometry.h`,最小记录型 POD:`move_to/line_to/cubic_to/translated/elements`) | 填充光栅化走 facade 安装的 `PathFillBackend` 钩子(同文件,inline 变量,默认 nullptr=不绘制),见 §7.10 | +| `QRectF` | `olive::RectF`(`node/gizmo/text.h`,gizmo 波次落地:仅 x/y/width/height 数据载体;translate/bounding 在使用点展开) | | +| `QPolygonF` | `std::vector` | | +| `QLineF` | `olive::LineF`(`node/gizmo/line.h`,gizmo 波次落地:两点 POD,`p1()/p2()`) | | +| `QTextDocument`/`QTextOption`/`QAbstractTextDocumentLayout`/`QFont`(文本节点排版+栅格化) | `TextLayoutRequest`/`TextLayoutSize`/`TextRenderTarget`/`TextRenderTransform` POD(`node/generator/text/textbackend.h`)+ `TextMeasureBackend`/`TextRenderBackend` 钩子 | k_font 输入值仍按上表存 family 字符串;钩子默认 nullptr=量测返回 0/不绘制,见 §7.10 | +| `Qt::Alignment`(gizmo 边界的对齐标志) | `int`,取值按 `TextGizmo::VerticalAlignment`(0=top/1=bottom/2=vcenter,gizmo 波次与 oakengine facade 对齐) | | +| `Qt::AltModifier`/`Qt::ShiftModifier`(gizmo_drag_move 位测试) | `int` 位测试,常量值同 Qt(`0x08000000`/`0x02000000`),用文件内 constexpr | gizmo 波次统一 | +| render 边界的字符串参数(`ShaderJob::insert`/`set_shader_id`、`AcceleratedJob::get`、`ShaderCode` 构造、`ShaderRequest::id` 等) | 直接传 `std::string`/字面量 | render 头当前仍是 Qt QString 版(M7 波次按 §6 stub 契约转 std::string),语法自查以 stub 为准 | +| `QLineF` | `olive::LineF`(`node/gizmo/line.h`,仅 p1()/p2() 数据载体) | 仅 gizmo 用 | +| `QRectF` | `olive::RectF`(`node/gizmo/text.h`,x()/y()/width()/height() 数据载体) | 仅文本 gizmo 矩形用 | +| `QPolygonF` | `std::vector` | `boundingRect()`/`containsPoint()` 由调用方(app/facade)自行实现 | +| `QPainterPath` | 删除 | 绘制图元,属 app 层;PathGizmo 只留类壳(层级/类型标识用) | +| `Qt::Alignment` | `int`(TextGizmo 垂直对齐:0=Top 1=Bottom 2=VCenter,与 oakengine facade 一致) | gizmo 波次对齐 | +| `QUuid`(Project/node uuid) | `std::string`,保留 QUuid 文本格式(带花括号 `{8-4-4-4-12}` 小写 hex);`QUuid::createUuid()` → 本地随机生成同格式字符串(v4/variant 位照设) | 读写均按原文本,工程文件兼容 | +| `QFileInfo::completeBaseName()` | `std::filesystem::path(p).filename()` 截取到第一个 `.` 为止 | | +| `QFileInfo::exists(p)` | `std::filesystem::exists(p, ec)` | | +| `QFileInfo(f).lastModified().toMSecsSinceEpoch()` | `std::filesystem::last_write_time` + file clock→system_clock 换算(`t - file_clock::now() + system_clock::now()`) | | +| `QFile` 整文件读/写 | `std::ifstream`/`std::ofstream`(binary)+ `std::stringstream`;`XmlStreamReader` 直接吃 `std::string` | | +| `qCompress`/`qUncompress` | zlib `compress2`/`uncompress` + 4 字节大端未压缩长度头(Qt 格式原样) | `.ove` 的 OVEC 段逐字节兼容,见 §7.11 | +| `QStandardPaths::CacheLocation` | macOS `$HOME/Library/Caches/oak`,否则 `FileFunctions::get_configuration_location()+"/cache"` | 仅 footage 探针缓存目录;位置变化只导致重新探针 | +| `QTimer` 周期回调(footage `check_footage`) | 删除定时器,函数本体保留为 public,由 facade 周期调用 | `qApp->activeWindow()` 门槛一并移到 app 层 | + +### 虚函数命名约定 + +Node 的虚函数(含事件钩子 `InputValueChangedEvent`/`InputConnectedEvent`/`InputDisconnectedEvent`/ +`OutputConnectedEvent`/`LoadFinishedEvent`/`AddedToGraphEvent` 等)**保持原 CamelCase 名字不变** +(虚函数 API 形状保持,仅换参数/返回类型)。子类 override 同理,不要 snake_case 化。 + +### NodeValueRow 访问 + +`NodeValueRow = std::map`,const 引用无 `operator[]`: +`value[k]` → `value.at(k)`(键必须存在,语义同 const `QHash::operator[]`)。 + +### QObject 父子机制的替代(keyframe / node 生命周期) + +- `NodeKeyframe` 持有 `Node *parent_`:构造参数传入,或 `set_parent(Node*)`。 + `Node::add_keyframe(key)` / `remove_keyframe(key)` 会自动 `set_parent(this)` / `set_parent(nullptr)`—— + 替代原 childEvent 的 ChildAdded/ChildRemoved 分支。**不要再对 keyframe 调 setParent()**。 +- `NodeInputImmediate::delete_all_keyframes(std::vector *reclaimed = nullptr)`: + 传 nullptr 即删除;传指针则把 keyframe 收回向量(替代原"reparent 到 memory_manager 续命")。 +- nodeundo 的命令用 `std::unique_ptr` / `std::vector>` 替代 + `QObject memory_manager_`(析构即删未交出的节点,undo 重新入图前 `release()`)。 +- 节点入图/出图:`graph_->add_node(node)` / `graph_->remove_node(node)`(Project 波次提供; + remove 是"摘出不删除")。 +- gizmo:`Node::add_gizmo()/remove_gizmo()` 替代 childEvent 的 gizmo 分支(gizmo 波次对齐)。 + +### include 路径写法(src/node/src 为根) + +- 本模块内:`"node/value.h"`、`"node/keyframe.h"` 等(CMake include root = `src/node/src`)。 +- oakcommon:`"xmlutils.h"`、`"debug.h"`、`"define.h"`(include dir = `src/common/src`,**没有** `common/` 前缀)。 +- oakundo:`"undocommand.h"`、`"undostack.h"`(include dir = `src/undo/src`)。 +- oakcore C++ 封装:`"olive/core/util/color.h"`、`"olive/core/util/bezier.h"`、`"olive/core/util/rational.h"`、`"olive/core/util/timerange.h"`、`"olive/core/util/stringutils.h"`。 +- `render/...`、`codec/...`、`pluginSupport/...`(OpenFX)include **原样保留**(M7/M9 处理),禁止新增。 + +### olive 命名空间别名(value.h 已建立,直接可用) + +```cpp +namespace olive { +using core::Bezier; // olive::core::Bezier +using core::Color; // olive::core::Color (float RGBA, red()/green()/blue()/alpha()) +using core::Rational; // olive::core::Rational (to_string()/from_string() 是 std::string) +} +``` + +`olive::core::TimeRange` 在 node.h 以 `using core::TimeRange;` 引入(见 node.h)。 + +## 2. Variant(QVariant 替代)速查 + +```cpp +#include "node/variant.h" // olive::Variant, olive::StringList, olive::ByteArray + +Variant v; // null,v.is_null() == true +Variant a = 42; // int(有符号统一存 int64_t) +Variant b = 3.14; // double(float 也存 double) +Variant c = std::string("x"); +Variant d = Vector2D(1, 2); // 任意可复制类型,类型擦除存储(原 Q_DECLARE_METATYPE 场景) + +v.value(); // 取数(QVariant::value()) +v.value(); // 自定义类型必须类型精确匹配,否则返回 T() +v.to_double(); v.to_float(); // QVariant 风格转换(数值互通、字符串解析) +v.to_int(); v.to_uint(); +v.to_long_long(); v.to_u_long_long(); +v.to_bool(); +v.to_string(); // double 按 %g 格式化(同 QString::number) +v.to_string_list(); // QStringList +v.to_byte_array(); // QByteArray +v.can_convert(); // QVariant::canConvert() +Variant::from_value(x); // QVariant::fromValue +v == w; // 数值跨 kind 按值比较;自定义类型用其 operator== +``` + +- `value()` → `value()` 或 `to_string()`。 +- 原来 `QVariant::fromValue(Color(...))` → `Variant::from_value(Color(...))`。 +- 函数返回 `QVariant` 的(如 `Node::get_standard_value()`)→ 返回 `Variant`,调用处照旧 `Variant v = ...; v.to_double()`。 + +## 3. 核心类新 API 形态 + +### NodeValue(`node/value.h`) + +```cpp +NodeValue v(NodeValue::k_float, 1.5, from_node); // 构造(模板,不变) +v.type(); // NodeValue::Type +v.value(); v.to_double(); v.to_string(); // 取数 +v.data(); // const Variant & +v.set_value(x); +NodeValue::value_to_string(type, variant, is_key_track); // std::string +NodeValue::string_to_value(type, str, is_key_track); // Variant +NodeValue::split_normal_value_into_track_values(type, v); // std::vector +NodeValue::combine_track_values_into_normal_value(type, split); // Variant +v.to_split_value(); // SplitValue = std::vector +``` + +### NodeValueTable + +`push/prepend/at/take_at/count/has/remove/clear/is_empty/get(type, tag)/merge(std::vector)`。 +`NodeValueRow = std::map`。 + +### Node(`node/node.h`) + +- 不再继承 QObject;纯虚 `name()/id()` 返回 `std::string`,`category()` 返回 `std::vector`, + `description()` 返回 `std::string`,`sub_category()` 返回 `std::string`。 +- 输入遍历:`for (const std::string &id : node->inputs())`。 +- `Position`:`PointF position; bool expanded;`,`load(XmlStreamReader*)` / `save(XmlStreamWriter*)`。 +- 序列化:`load(XmlStreamReader*, SerializedData*)` / `save(XmlStreamWriter*)`。 +- gizmo/undo 等签名里的 `MultiUndoCommand` 来自 oakundo(`undocommand.h`)。 + +### NodeKeyframe(`node/keyframe.h`) + +- `bezier_control_in()/out()` 返回 `const PointF &`;不再有任何 signal。 +- 值类型:`Variant value()` / `set_value(const Variant&)`。 + +## 4. 被删除的东西(第一波) + +### Node 的 signals(整组删除,facade 层经 oakengine_event 发通知) + +label_changed、color_changed、value_changed、input_connected、input_disconnected、 +output_connected、output_disconnected、input_value_hint_changed、input_property_changed、 +links_changed、input_array_size_changed、keyframe_added、keyframe_removed、 +keyframe_time_changed、message_count_changed、keyframe_type_changed、 +keyframe_value_changed、keyframe_enable_changed、input_added、input_removed、 +input_name_changed、input_data_type_changed、added_to_graph、removed_from_graph、 +node_added_to_context、node_position_in_context_changed、node_removed_from_context、 +input_flags_changed。 + +### NodeKeyframe 的 signals + +value_changed、time_changed、type_changed、bezier_control_in_changed、 +bezier_control_out_changed(删除理由同上)。 + +### UI 绘制(属 app 层) + +`Node::gradient_color()`、`Node::brush()`(QLinearGradient/QBrush);`Node::color()` 保留(返回 olive::core::Color 数据)。 +`Node::gizmo_transformation()` 的 QTransform 改 Matrix4x4(数据类型,不是绘制)。 + +### 其他 + +- `childEvent(QChildEvent*)`(QObject 事件机制)——keyframe 分支变 `Node::add_keyframe()/remove_keyframe()`, + gizmo 分支变 `Node::add_gizmo()/remove_gizmo()`。 +- `Q_DECLARE_METATYPE`、`qHash()` 重载。 +- nodeundo 的 23 处 `get_relevant_project()` override(modified 语义由 oakundo 回调承担)。 +- `NodeAddCommand::push_to_thread(QThread*)`(QObject 线程亲和)。 +- render cache 的 `QUuid` uuid:改 `std::string`(见映射表)。 +- keyframe 失效通知链:原 keyframe signal→Node slot 的 5 条(invalidate_from_keyframe_*)随 signal 删除, + 函数本体保留为 public 成员(带 `NodeKeyframe *key` 参数替代 sender()),**调用方由 facade/keyframe 波次接**。 + +## 5. undo(oakundo)适配 + +- `#include "undocommand.h"`,`olive::UndoCommand` / `olive::MultiUndoCommand`。 +- **没有** `get_relevant_project()`:原来 `get_relevant_project()->set_modified(true)` 的语义由 + `UndoCommand::set_modified_callbacks(is_modified, set_modified)` + + `redo_and_set_modified()/undo_and_set_modified()` 承担。nodeundo 的命令类不再碰 Project 的 + modified 标记,回调由 facade 层装配。 +- `prepare()` 仍是 protected virtual;`UndoStack::push(cmd)` 语义不变(见 oakundo undostack.h)。 + +## 6. 语法自检 + +整库编译本波必然失败(叶子未改),但每个改过的 .cpp 必须过 `-fsyntax-only`(缺失的 render/ 等头用 stub 垫): + +```bash +# stub 头在 /tmp/oakstub(render/texture.h 等,仅语法检查用,不进仓库) +cd src/node/src/node && c++ -std=c++17 -fsyntax-only -Wall \ + -I. -I.. -I/tmp/oakstub \ + -I$OAK/src/common/src -I$OAK/core/include -I$OAK/src/undo/src \ + -I/opt/homebrew/include -I/opt/homebrew/include/Imath \ + -I$OAK/third_party/openfx/include -I$OAK/third_party/openfx/HostSupport/include \ + +``` + +(`$OAK` = /Users/sunyu/Projects/oak) + +## 7. 已知行为注意点与后续波次依赖 + +1. `NodeValueDatabase::merge()`:原 `QHash::values()` 无序,现按 key 字典序收集后合并—— + merge 是按优先级覆盖语义,若调用方依赖原哈希序需注意。 +2. `NodeValueTable` 新增了 `operator==/!=`(Variant 存 `NodeValueTableArray` 需要)。 +3. `FrameHashCache(this)` 等 cache 构造仍传 `this`(现在是 `Node *` 而非 `QObject *`)—— + M7 定型 render cache 类时构造参数需接受 `Node *`(或届时改 nullptr,需裁决)。 +4. `traverser.h` 新增 `TimeRangeLess` 比较器(`QHash`→`std::map` 需要严格弱序, + 按 `(in(), out())` 排序)。 +5. nodeundo 假定 `Project::add_node/remove_node/is_being_cleared/ + get_number_of_contexts_node_is_in(node,bool)` 存在(project 波次提供,M3 手册 §2 已冻结 + add/remove_node 函数族)。 +6. `factory.cpp` 的 `pluginSupport/` include、`traverser.h` 的 `"common/cancelableobject.h"` + 原样保留(后者 oakcommon 没有,留 M 系列裁决)。 +7. `node.h` 仍 include `config/config.h`、`ui/colorcoding.h`(engine Qt 头,`color()` 用到 + `OAK_CONFIG_STR(...)`)——config 波次处理。 +8. `Node::gizmo_drag_move` 的 modifiers 参数为 `int`(原 `Qt::KeyboardModifiers`)。 +9. timeformat 的 `format_date_time()` 不实现 `MMMM`(月名)/`dddd`(星期名)本地化 token + (原默认格式 `hh:mm:ss` 用不到);负 epoch 毫秒的 `zzz` 取模与 Qt 有边界差异(实际输入不会触发)。 +10. generator 波次(matrix/noise/polygon/shape/solid/text/multicam): + - `gizmo_drag_move` 原 slot 用 `sender()` 取被拖 gizmo;gizmo 波次定型为 + `Node::current_gizmo()`(DraggableGizmo 直调 3 参虚函数期间设置),本波照此收口。 + - `new PathGizmo(this)`/`new TextGizmo(this)` 后必须显式 `add_gizmo(...)` + (NodeGizmo 构造不再自注册;`add_draggable_gizmo<>()` 内部已含)。 + - PathGizmo 不再存储路径(gizmo 波次:绘制数据归 app 层),polygon 原 + `poly_gizmo_->set_path(...)` 调用删除;`generate_path()` 仍供 generate_frame 使用。 + - polygon/text 的 `generate_frame()` 光栅化(QPainter/QTextDocument)委托给 + `PathFillBackend`/`TextMeasureBackend`+`TextRenderBackend` 钩子(geometry.h / + textbackend.h);facade 未安装后端前输出为空白(被迫行为差异,facade 波次恢复)。 + 像素缓冲清零、alpha 移植循环等纯数据逻辑原样保留。 + - textv2 非 SSE 死分支里的 `VideoParams::kRGBAChannelCount` 拼写在任何 Qt 头中都不存在 + (原代码靠 Q_PROCESSOR_X86/ARM 宏永远不编译该分支),改为 `k_rgba_channel_count`。 +10. 叶子效果波次(audio/distort/effect/filter/keying)新增约定(见映射表新增行): + `Matrix4x4::map(PointF)`、`Vector2D::to_point_f()` 为 mathtypes.h 增量补充; + `Node::current_gizmo()`/`set_current_gizmo()` 为 node.h 增量补充,替代 gizmo 回调中的 + `sender()`;gizmo 波次须在 DraggableGizmo 直接调用 drag 回调时包一层 + `set_current_gizmo(this)` / `set_current_gizmo(nullptr)`。 + gizmo 头(`node/gizmo/polygon.h` 的 `set_polygon(QPolygonF)`、`point.h`/`text.h` 的 + QRectF/QTransform 接口)仍属 gizmo 波次,叶子文件按新类型(`std::vector`)调用, + 签名对齐由 gizmo 波次完成。 +10. gizmo 波次:`NodeGizmo` 构造**不自动**向 parent 注册(`Node::add_draggable_gizmo()` 已显式 + `add_gizmo()`);析构时若持有 parent 则 `remove_gizmo()`(对应原 dtor 的 `setParent(nullptr)`)。 + 直接 `new XGizmo(node)` 的旧调用点(polygon/textv3 等)改完信号后需补 `add_gizmo()`。 + `DraggableGizmo` 的 handle_start/handle_movement 信号改为直调 + `parent_node()->gizmo_drag_start()/gizmo_drag_move()`。各 gizmo 的 `draw(QPainter*)`、 + `PointGizmo::get_clicking_rect()/get_drawing_rect()/get_standard_radius()` + (依赖 `QFontMetrics(qApp->font())`)删除,归 app 层(facade 的 hit-test 需在 app 侧重实现)。 + `TextGizmo` 的 4 个信号(activated/deactivated/rect_changed/vertical_alignment_changed)删除, + 由 oakengine 事件机制承担。 +11. project 波次(project/folder/footage/sequence/serializer): + - `Project` 不再继承 QObject:`childEvent` 的 ChildAdded/ChildRemoved 分支变 + `Project::add_node(node)` / `remove_node(node)`(remove 摘出不删除);节点所有权归 + Project(`clear()`/析构删除)。`ColorManager` 由 `std::unique_ptr` 持有(原 QObject 父子)。 + - `Project`/`Folder`/`Sequence`/`Footage` 的信号整组删除(含 Folder 的 + begin/end_insert/remove_item、Sequence 的 track_added/removed/subtitles_changed、 + Footage 的 proxy_settings_changed、Project 的 name_changed/modified_changed/ + setting_changed/node_added/node_removed 等),通知由 facade 层承担。 + - `Sequence::update_track_cache()` 变 public 成员;TrackList 波次须在原 + track_list_changed/length_changed 发射点直调 `sequence->update_track_cache()` / + `verify_length()`。`Footage::check_footage()/default_color_space_changed()/ + proxy_ready()/proxy_finished()` 同理保留为 public 待 facade/ProxyManager 波次接线。 + - 序列化 XML 元素/属性名与读写顺序逐字节保持不变;`XmlStreamWriter` 不再输出 + XML 声明与自动缩进(紧凑 XML),新旧 reader 均兼容。OVEC 压缩段为 + qCompress 兼容格式(zlib + 4 字节大端长度)。 + - `Footage::generate_frame()` 的离线媒体警示帧:QImage/QPainter 光栅化改为纯像素 + 循环(深红底+斜纹),文字叠层("Media Offline")与抗锯齿丢失(被迫行为差异)。 + - timeline 边界(M4):`TimelineMarker`/`TimelineWorkArea`/`TimelineMarkerList` 调用按 + 去 Qt 形态书写(std::string、XmlStreamReader/Writer),签名对齐由 M4 完成。 diff --git a/src/node/c_api/CMakeLists.txt b/src/node/c_api/CMakeLists.txt new file mode 100644 index 000000000..2a8b2a18a --- /dev/null +++ b/src/node/c_api/CMakeLists.txt @@ -0,0 +1,15 @@ +target_sources(oaknode PRIVATE + block.cpp + colormanager.cpp + factory.cpp + folder.cpp + footage.cpp + group.cpp + keyframe.cpp + node.cpp + project.cpp + sequence.cpp + serializer.cpp + track.cpp + traverser.cpp +) diff --git a/src/node/c_api/alivecount.h b/src/node/c_api/alivecount.h new file mode 100644 index 000000000..1514dfa7c --- /dev/null +++ b/src/node/c_api/alivecount.h @@ -0,0 +1,39 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EDITOR_NODE_ALIVECOUNT_H +#define OAK_EDITOR_NODE_ALIVECOUNT_H + +/** + * @brief Shared live-object counter hooks (internal, not installed). + * + * The counter itself and the public oaknode_debug_alive_count() live in + * the node family (src/node/c_api/node.cpp); these hooks have external + * linkage so the other families' create/free functions can participate. + */ +namespace oaknode_c_api +{ + +void alive_inc(); +void alive_dec(); + +} + +#endif //OAK_EDITOR_NODE_ALIVECOUNT_H diff --git a/src/node/c_api/block.cpp b/src/node/c_api/block.cpp new file mode 100644 index 000000000..bd3a1d9fc --- /dev/null +++ b/src/node/c_api/block.cpp @@ -0,0 +1,477 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/block.h" + +#include "alivecount.h" + +#include "block/block.h" +#include "block/clip/clip.h" +#include "block/gap/gap.h" +#include "block/transition/crossdissolve/crossdissolvetransition.h" +#include "block/transition/diptocolor/diptocolortransition.h" +#include "block/transition/transition.h" +#include "output/track/track.h" + +namespace +{ + +olive::Block *impl(OakNodeBlock *h) +{ + return reinterpret_cast(h); +} + +olive::ClipBlock *clip_impl(OakNodeBlock *h) +{ + return h ? dynamic_cast(impl(h)) : nullptr; +} + +olive::TransitionBlock *transition_impl(OakNodeBlock *h) +{ + return h ? dynamic_cast(impl(h)) : nullptr; +} + +OakNodeBlock *wrap(olive::Block *b) +{ + return reinterpret_cast(b); +} + +int get_rational(const olive::core::Rational &r, int *numerator, + int *denominator) +{ + if (!numerator || !denominator) { + return OAKNODE_E_INVALID; + } + *numerator = r.numerator(); + *denominator = r.denominator(); + return OAKNODE_OK; +} + +template +OakNodeBlock *create_block(Args &&...args) +{ + try { + T *b = new T(std::forward(args)...); + oaknode_c_api::alive_inc(); + return wrap(b); + } catch (...) { + return nullptr; + } +} + +} // namespace + +OakNodeBlock *oaknode_block_clip_create(void) +{ + return create_block(); +} + +OakNodeBlock *oaknode_block_gap_create(void) +{ + return create_block(); +} + +OakNodeBlock *oaknode_block_transition_create(int kind) +{ + switch (kind) { + case OAKNODE_TRANSITION_CROSS_DISSOLVE: + return create_block(); + case OAKNODE_TRANSITION_DIP_TO_COLOR: + return create_block(); + default: + return nullptr; + } +} + +void oaknode_block_free(OakNodeBlock *block) +{ + if (!block) { + return; + } + delete impl(block); + oaknode_c_api::alive_dec(); +} + +int oaknode_block_get_in(OakNodeBlock *block, int *numerator, int *denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(block)->in(), numerator, denominator); +} + +int oaknode_block_set_in(OakNodeBlock *block, int numerator, int denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + impl(block)->set_in(olive::core::Rational(numerator, denominator)); + return OAKNODE_OK; +} + +int oaknode_block_get_out(OakNodeBlock *block, int *numerator, int *denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(block)->out(), numerator, denominator); +} + +int oaknode_block_set_out(OakNodeBlock *block, int numerator, int denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + impl(block)->set_out(olive::core::Rational(numerator, denominator)); + return OAKNODE_OK; +} + +int oaknode_block_get_length(OakNodeBlock *block, int *numerator, + int *denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(block)->length(), numerator, denominator); +} + +int oaknode_block_set_length_and_media_out(OakNodeBlock *block, int numerator, + int denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + try { + impl(block)->set_length_and_media_out( + olive::core::Rational(numerator, denominator)); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_block_set_length_and_media_in(OakNodeBlock *block, int numerator, + int denominator) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + try { + impl(block)->set_length_and_media_in( + olive::core::Rational(numerator, denominator)); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_block_get_enabled(OakNodeBlock *block, int *enabled) +{ + if (!block || !enabled) { + return OAKNODE_E_INVALID; + } + *enabled = impl(block)->is_enabled() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_block_set_enabled(OakNodeBlock *block, int enabled) +{ + if (!block) { + return OAKNODE_E_INVALID; + } + impl(block)->set_enabled(enabled != 0); + return OAKNODE_OK; +} + +int oaknode_block_get_previous(OakNodeBlock *block, OakNodeBlock **out) +{ + if (!block || !out) { + return OAKNODE_E_INVALID; + } + *out = wrap(impl(block)->previous()); + return OAKNODE_OK; +} + +int oaknode_block_get_next(OakNodeBlock *block, OakNodeBlock **out) +{ + if (!block || !out) { + return OAKNODE_E_INVALID; + } + *out = wrap(impl(block)->next()); + return OAKNODE_OK; +} + +int oaknode_block_get_track(OakNodeBlock *block, OakNodeTrack **out) +{ + if (!block || !out) { + return OAKNODE_E_INVALID; + } + *out = reinterpret_cast(impl(block)->track()); + return OAKNODE_OK; +} + +int oaknode_block_link(OakNodeBlock *a, OakNodeBlock *b) +{ + if (!a || !b) { + return OAKNODE_E_INVALID; + } + return olive::Node::link(impl(a), impl(b)) ? OAKNODE_OK : OAKNODE_E_FAILED; +} + +int oaknode_block_unlink(OakNodeBlock *a, OakNodeBlock *b) +{ + if (!a || !b) { + return OAKNODE_E_INVALID; + } + return olive::Node::unlink(impl(a), impl(b)) ? OAKNODE_OK : OAKNODE_E_FAILED; +} + +int oaknode_block_are_linked(OakNodeBlock *a, OakNodeBlock *b, int *linked) +{ + if (!a || !b || !linked) { + return OAKNODE_E_INVALID; + } + *linked = olive::Node::are_linked(impl(a), impl(b)) ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_block_get_link_count(OakNodeBlock *block, int *count) +{ + if (!block || !count) { + return OAKNODE_E_INVALID; + } + *count = int(impl(block)->links().size()); + return OAKNODE_OK; +} + +int oaknode_block_get_link_at(OakNodeBlock *block, int index, + OakNodeBlock **out) +{ + if (!block || !out || index < 0) { + return OAKNODE_E_INVALID; + } + const auto &links = impl(block)->links(); + if (index >= int(links.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out = wrap(static_cast(links.at(index))); + return OAKNODE_OK; +} + +/* ---------------------------------------------------------------- Clip */ + +int oaknode_clip_get_media_in(OakNodeBlock *clip, int *numerator, + int *denominator) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + return get_rational(c->media_in(), numerator, denominator); +} + +int oaknode_clip_set_media_in(OakNodeBlock *clip, int numerator, + int denominator) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + c->set_media_in(olive::core::Rational(numerator, denominator)); + return OAKNODE_OK; +} + +int oaknode_clip_get_speed(OakNodeBlock *clip, double *speed) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c || !speed) { + return OAKNODE_E_INVALID; + } + *speed = c->speed(); + return OAKNODE_OK; +} + +int oaknode_clip_set_speed(OakNodeBlock *clip, double speed) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + c->set_standard_value(olive::ClipBlock::k_speed_input, speed); + return OAKNODE_OK; +} + +int oaknode_clip_get_reverse(OakNodeBlock *clip, int *reverse) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c || !reverse) { + return OAKNODE_E_INVALID; + } + *reverse = c->reverse() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_clip_set_reverse(OakNodeBlock *clip, int reverse) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + c->set_reverse(reverse != 0); + return OAKNODE_OK; +} + +int oaknode_clip_get_maintain_audio_pitch(OakNodeBlock *clip, int *maintain) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c || !maintain) { + return OAKNODE_E_INVALID; + } + *maintain = c->maintain_audio_pitch() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_clip_set_maintain_audio_pitch(OakNodeBlock *clip, int maintain) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + c->set_maintain_audio_pitch(maintain != 0); + return OAKNODE_OK; +} + +int oaknode_clip_get_loop_mode(OakNodeBlock *clip, int *loop_mode) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c || !loop_mode) { + return OAKNODE_E_INVALID; + } + *loop_mode = int(c->loop_mode()); + return OAKNODE_OK; +} + +int oaknode_clip_set_loop_mode(OakNodeBlock *clip, int loop_mode) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c) { + return OAKNODE_E_INVALID; + } + c->set_loop_mode(static_cast(loop_mode)); + return OAKNODE_OK; +} + +int oaknode_clip_get_track_type(OakNodeBlock *clip, int *type) +{ + olive::ClipBlock *c = clip_impl(clip); + if (!c || !type) { + return OAKNODE_E_INVALID; + } + *type = int(c->get_track_type()); + return OAKNODE_OK; +} + +/* ----------------------------------------------------------- Transition */ + +int oaknode_transition_get_in_offset(OakNodeBlock *transition, int *numerator, + int *denominator) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t) { + return OAKNODE_E_INVALID; + } + return get_rational(t->in_offset(), numerator, denominator); +} + +int oaknode_transition_get_out_offset(OakNodeBlock *transition, int *numerator, + int *denominator) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t) { + return OAKNODE_E_INVALID; + } + return get_rational(t->out_offset(), numerator, denominator); +} + +int oaknode_transition_get_offset_center(OakNodeBlock *transition, + int *numerator, int *denominator) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t) { + return OAKNODE_E_INVALID; + } + return get_rational(t->offset_center(), numerator, denominator); +} + +int oaknode_transition_set_offset_center(OakNodeBlock *transition, + int numerator, int denominator) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t) { + return OAKNODE_E_INVALID; + } + t->set_offset_center(olive::core::Rational(numerator, denominator)); + return OAKNODE_OK; +} + +int oaknode_transition_set_offsets_and_length(OakNodeBlock *transition, + int in_num, int in_den, + int out_num, int out_den) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t) { + return OAKNODE_E_INVALID; + } + t->set_offsets_and_length(olive::core::Rational(in_num, in_den), + olive::core::Rational(out_num, out_den)); + return OAKNODE_OK; +} + +int oaknode_transition_is_dual(OakNodeBlock *transition, int *dual) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t || !dual) { + return OAKNODE_E_INVALID; + } + *dual = t->is_dual_transition() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_transition_get_connected_out_block(OakNodeBlock *transition, + OakNodeBlock **out) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t || !out) { + return OAKNODE_E_INVALID; + } + *out = wrap(t->connected_out_block()); + return OAKNODE_OK; +} + +int oaknode_transition_get_connected_in_block(OakNodeBlock *transition, + OakNodeBlock **out) +{ + olive::TransitionBlock *t = transition_impl(transition); + if (!t || !out) { + return OAKNODE_E_INVALID; + } + *out = wrap(t->connected_in_block()); + return OAKNODE_OK; +} diff --git a/src/node/c_api/colormanager.cpp b/src/node/c_api/colormanager.cpp new file mode 100644 index 000000000..2029bc870 --- /dev/null +++ b/src/node/c_api/colormanager.cpp @@ -0,0 +1,369 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/colormanager.h" + +#include + +#include "alivecount.h" + +#include "color/colormanager/colormanager.h" +#include "colortransform.h" +#include "project.h" + +// Same handle-echo pattern as sequence.cpp: oakcommon defines +// `struct OakCommonColorTransform { olive::ColorTransform impl; }` +// (src/common/c_api/colortransform.cpp) without exporting the definition. +struct OakCommonColorTransform { + olive::ColorTransform impl; +}; + +struct OakNodeColorManager { + olive::ColorManager impl; +}; + +namespace +{ + +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int needed = int(value.size()) + 1; + if (buf && buf_size >= needed) { + memcpy(buf, value.c_str(), needed); + } + return needed; +} + +bool has_config(olive::ColorManager *cm) +{ + return cm && cm->get_config() != nullptr; +} + +int list_at(const olive::StringList &list, int index, char *buf, int buf_size) +{ + if (index < 0 || index >= int(list.size())) { + return OAKNODE_E_NOT_FOUND; + } + return copy_string(list.at(index), buf, buf_size); +} + +} // namespace + +OakNodeColorManager *oaknode_colormanager_init(OakNodeProject *project) +{ + if (!project) { + return nullptr; + } + try { + auto *m = new OakNodeColorManager{ + olive::ColorManager(reinterpret_cast(project))}; + oaknode_c_api::alive_inc(); + return m; + } catch (...) { + return nullptr; + } +} + +void oaknode_colormanager_free(OakNodeColorManager *manager) +{ + if (!manager) { + return; + } + delete manager; + oaknode_c_api::alive_dec(); +} + +int oaknode_colormanager_initialize(OakNodeColorManager *manager) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + try { + manager->impl.init(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_colormanager_set_up_default_config(void) +{ + try { + olive::ColorManager::set_up_default_config(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_colormanager_get_config_filename(OakNodeColorManager *manager, + char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + return copy_string(manager->impl.get_config_filename(), buf, buf_size); +} + +int oaknode_colormanager_set_config_filename(OakNodeColorManager *manager, + const char *filename) +{ + if (!manager || !filename) { + return OAKNODE_E_INVALID; + } + manager->impl.set_config_filename(filename); + return OAKNODE_OK; +} + +int oaknode_colormanager_update_config_from_filename( + OakNodeColorManager *manager) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + try { + manager->impl.update_config_from_filename(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_colormanager_get_default_input_color_space( + OakNodeColorManager *manager, char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + return copy_string(manager->impl.get_default_input_color_space(), buf, + buf_size); +} + +int oaknode_colormanager_set_default_input_color_space( + OakNodeColorManager *manager, const char *colorspace) +{ + if (!manager || !colorspace) { + return OAKNODE_E_INVALID; + } + manager->impl.set_default_input_color_space(colorspace); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_reference_color_space( + OakNodeColorManager *manager, char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + return copy_string(manager->impl.get_reference_color_space(), buf, + buf_size); +} + +int oaknode_colormanager_get_compliant_color_space( + OakNodeColorManager *manager, const char *colorspace, char *buf, + int buf_size) +{ + if (!manager || !colorspace) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return copy_string(manager->impl.get_compliant_color_space(colorspace), buf, + buf_size); +} + +int oaknode_colormanager_get_colorspace_for_ffmpeg_tags( + OakNodeColorManager *manager, int primaries, int trc, char *buf, + int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return copy_string( + manager->impl.get_colorspace_for_ffmpeg_tags(primaries, trc), buf, + buf_size); +} + +int oaknode_colormanager_get_display_count(OakNodeColorManager *manager, + int *count) +{ + if (!manager || !count) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + *count = int(manager->impl.list_available_displays().size()); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_display_at(OakNodeColorManager *manager, + int index, char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return list_at(manager->impl.list_available_displays(), index, buf, + buf_size); +} + +int oaknode_colormanager_get_default_display(OakNodeColorManager *manager, + char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return copy_string(manager->impl.get_default_display(), buf, buf_size); +} + +int oaknode_colormanager_get_view_count(OakNodeColorManager *manager, + const char *display, int *count) +{ + if (!manager || !display || !count) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + *count = int(manager->impl.list_available_views(display).size()); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_view_at(OakNodeColorManager *manager, + const char *display, int index, char *buf, + int buf_size) +{ + if (!manager || !display) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return list_at(manager->impl.list_available_views(display), index, buf, + buf_size); +} + +int oaknode_colormanager_get_default_view(OakNodeColorManager *manager, + const char *display, char *buf, + int buf_size) +{ + if (!manager || !display) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return copy_string(manager->impl.get_default_view(display), buf, buf_size); +} + +int oaknode_colormanager_get_look_count(OakNodeColorManager *manager, + int *count) +{ + if (!manager || !count) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + *count = int(manager->impl.list_available_looks().size()); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_look_at(OakNodeColorManager *manager, int index, + char *buf, int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return list_at(manager->impl.list_available_looks(), index, buf, buf_size); +} + +int oaknode_colormanager_get_colorspace_count(OakNodeColorManager *manager, + int *count) +{ + if (!manager || !count) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + *count = int(manager->impl.list_available_colorspaces().size()); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_colorspace_at(OakNodeColorManager *manager, + int index, char *buf, + int buf_size) +{ + if (!manager) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + return list_at(manager->impl.list_available_colorspaces(), index, buf, + buf_size); +} + +int oaknode_colormanager_get_default_luma_coefs(OakNodeColorManager *manager, + double rgb[3]) +{ + if (!manager || !rgb) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + manager->impl.get_default_luma_coefs(rgb); + return OAKNODE_OK; +} + +int oaknode_colormanager_get_compliant_color_transform( + OakNodeColorManager *manager, const OakCommonColorTransform *transform, + int force_display, OakCommonColorTransform **out) +{ + if (!manager || !transform || !out) { + return OAKNODE_E_INVALID; + } + if (!has_config(&manager->impl)) { + return OAKNODE_E_STATE; + } + try { + *out = new OakCommonColorTransform{ + manager->impl.get_compliant_color_space(transform->impl, + force_display != 0)}; + } catch (...) { + return OAKNODE_E_NOMEM; + } + return OAKNODE_OK; +} diff --git a/src/node/c_api/factory.cpp b/src/node/c_api/factory.cpp new file mode 100644 index 000000000..5c2c247fe --- /dev/null +++ b/src/node/c_api/factory.cpp @@ -0,0 +1,144 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/factory.h" + +#include "factory.h" + +#include "valueconvert.h" + +namespace +{ + +inline OakNodeNode *from_node(olive::Node *node) +{ + return reinterpret_cast(node); +} + +} + +int oaknode_factory_initialize(void) +{ + try { + if (olive::NodeFactory::get_library().empty()) { + olive::NodeFactory::initialize(); + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +void oaknode_factory_destroy(void) +{ + try { + olive::NodeFactory::destroy(); + } catch (...) { + } +} + +int oaknode_factory_id_count(int *out_count) +{ + if (!out_count) { + return OAKNODE_E_INVALID; + } + + try { + if (olive::NodeFactory::get_library().empty()) { + return OAKNODE_E_STATE; + } + *out_count = static_cast(olive::NodeFactory::get_library().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_factory_id_at(int index, char *buf, int buf_size) +{ + try { + const std::vector &library = + olive::NodeFactory::get_library(); + if (library.empty()) { + return OAKNODE_E_STATE; + } + if (index < 0 || index >= static_cast(library.size())) { + return OAKNODE_E_NOT_FOUND; + } + return oaknode_c_api::copy_string(library[size_t(index)]->id(), buf, + buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_factory_name_from_id(const char *type_id, char *buf, + int buf_size) +{ + if (!type_id) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string( + olive::NodeFactory::get_name_from_id(type_id), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeNode *oaknode_factory_create_from_id(const char *type_id) +{ + if (!type_id) { + return NULL; + } + + try { + olive::Node *node = olive::NodeFactory::create_from_id(type_id); + if (node) { + oaknode_c_api::alive_inc(); + } + return from_node(node); + } catch (...) { + return NULL; + } +} + +int oaknode_factory_node_at(int index, OakNodeNode **out_node) +{ + if (!out_node) { + return OAKNODE_E_INVALID; + } + + try { + const std::vector &library = + olive::NodeFactory::get_library(); + if (library.empty()) { + return OAKNODE_E_STATE; + } + if (index < 0 || index >= static_cast(library.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out_node = from_node(library[size_t(index)]); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/folder.cpp b/src/node/c_api/folder.cpp new file mode 100644 index 000000000..ed1c89728 --- /dev/null +++ b/src/node/c_api/folder.cpp @@ -0,0 +1,232 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/folder.h" + +#include + +#include "../src/project.h" +#include "../src/project/folder/folder.h" + +namespace +{ + +olive::Folder *to_cpp(OakNodeFolder *folder) +{ + return reinterpret_cast(folder); +} + +const olive::Folder *to_cpp(const OakNodeFolder *folder) +{ + return reinterpret_cast(folder); +} + +olive::Node *to_cpp(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +const olive::Node *to_cpp(const OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +OakNodeNode *to_c(olive::Node *node) +{ + return reinterpret_cast(node); +} + +olive::Project *to_cpp(OakNodeProject *project) +{ + return reinterpret_cast(project); +} + +} // namespace + +OakNodeFolder *oaknode_folder_create(OakNodeProject *project) +{ + if (!project) { + return NULL; + } + + try { + auto *folder = new (std::nothrow) olive::Folder(); + if (!folder) { + return NULL; + } + to_cpp(project)->add_node(folder); + return reinterpret_cast(folder); + } catch (...) { + return NULL; + } +} + +int oaknode_folder_child_count(const OakNodeFolder *folder) +{ + if (!folder) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(folder)->item_child_count(); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeNode *oaknode_folder_child_at(const OakNodeFolder *folder, int index) +{ + if (!folder || index < 0 || index >= to_cpp(folder)->item_child_count()) { + return NULL; + } + + try { + return to_c(to_cpp(folder)->item_child(index)); + } catch (...) { + return NULL; + } +} + +int oaknode_folder_add_child(OakNodeFolder *folder, OakNodeNode *child) +{ + if (!folder || !child) { + return OAKNODE_E_INVALID; + } + + try { + olive::Folder *f = to_cpp(folder); + olive::Node *c = to_cpp(child); + + if (c->folder()) { + return OAKNODE_E_STATE; + } + + olive::FolderAddChild cmd(f, c); + cmd.redo_now(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_folder_remove_child(OakNodeFolder *folder, OakNodeNode *child) +{ + if (!folder || !child) { + return OAKNODE_E_INVALID; + } + + try { + olive::Folder *f = to_cpp(folder); + olive::Node *c = to_cpp(child); + + if (f->index_of_child(c) == -1) { + return OAKNODE_E_NOT_FOUND; + } + + olive::Folder::RemoveElementCommand cmd(f, c); + cmd.redo_now(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_folder_move_children(OakNodeNode *const *nodes, int count, + OakNodeFolder *dest_folder) +{ + if (!nodes || count < 0 || !dest_folder) { + return OAKNODE_E_INVALID; + } + + try { + olive::Folder *dest = to_cpp(dest_folder); + + for (int i = 0; i < count; i++) { + if (!nodes[i]) { + return OAKNODE_E_INVALID; + } + + olive::Node *node = to_cpp(nodes[i]); + olive::Folder *old_folder = node->folder(); + + if (old_folder == dest) { + continue; + } + + if (old_folder) { + olive::Folder::RemoveElementCommand remove_cmd(old_folder, node); + remove_cmd.redo_now(); + } + + olive::FolderAddChild add_cmd(dest, node); + add_cmd.redo_now(); + } + + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_folder_has_child_recursive(const OakNodeFolder *folder, + const OakNodeNode *child) +{ + if (!folder || !child) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(folder)->has_child_recursive( + const_cast(to_cpp(child))) + ? 1 + : 0; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_folder_index_of_child(const OakNodeFolder *folder, + const OakNodeNode *child) +{ + if (!folder || !child) { + return OAKNODE_E_INVALID; + } + + try { + int index = to_cpp(folder)->index_of_child( + const_cast(to_cpp(child))); + return index == -1 ? OAKNODE_E_NOT_FOUND : index; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeFolder *oaknode_folder_parent_of(const OakNodeNode *node) +{ + if (!node) { + return NULL; + } + + try { + return reinterpret_cast(to_cpp(node)->folder()); + } catch (...) { + return NULL; + } +} diff --git a/src/node/c_api/footage.cpp b/src/node/c_api/footage.cpp new file mode 100644 index 000000000..220d65df9 --- /dev/null +++ b/src/node/c_api/footage.cpp @@ -0,0 +1,322 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/footage.h" + +#include +#include +#include + +#include "../src/project.h" +#include "../src/project/footage/footage.h" + +namespace +{ + +olive::Footage *to_cpp(OakNodeFootage *footage) +{ + return reinterpret_cast(footage); +} + +const olive::Footage *to_cpp(const OakNodeFootage *footage) +{ + return reinterpret_cast(footage); +} + +olive::Project *to_cpp(OakNodeProject *project) +{ + return reinterpret_cast(project); +} + +/** + * @brief Shared two-stage string getter. + * + * Returns the required buffer size in bytes (including the terminating + * NUL) as a non-negative value. + */ +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + + if (buf && buf_size > 0) { + size_t copy_len = value.size(); + if (copy_len > static_cast(buf_size) - 1) { + copy_len = static_cast(buf_size) - 1; + } + memcpy(buf, value.data(), copy_len); + buf[copy_len] = '\0'; + } + + return required; +} + +} // namespace + +OakNodeFootage *oaknode_footage_create(OakNodeProject *project, + const char *filename) +{ + if (!project) { + return NULL; + } + + try { + auto *footage = new (std::nothrow) + olive::Footage(filename ? filename : ""); + if (!footage) { + return NULL; + } + to_cpp(project)->add_node(footage); + return reinterpret_cast(footage); + } catch (...) { + return NULL; + } +} + +int oaknode_footage_filename(const OakNodeFootage *footage, char *buf, + int buf_size) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(footage)->filename(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_set_filename(OakNodeFootage *footage, const char *filename) +{ + if (!footage || !filename) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(footage)->set_filename(filename); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_is_valid(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + return to_cpp(footage)->is_valid() ? 1 : 0; +} + +int oaknode_footage_timestamp(const OakNodeFootage *footage, + int64_t *out_timestamp) +{ + if (!footage || !out_timestamp) { + return OAKNODE_E_INVALID; + } + + try { + *out_timestamp = to_cpp(footage)->timestamp(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_set_timestamp(OakNodeFootage *footage, int64_t timestamp) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(footage)->set_timestamp(timestamp); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_decoder(const OakNodeFootage *footage, char *buf, + int buf_size) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(footage)->decoder(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_total_stream_count(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(footage)->get_total_stream_count(); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_video_stream_count(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(footage)->get_video_stream_count(); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_audio_stream_count(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(footage)->get_audio_stream_count(); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_subtitle_stream_count(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return to_cpp(footage)->get_subtitle_stream_count(); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_duration(const OakNodeFootage *footage, int *out_numerator, + int *out_denominator) +{ + if (!footage || !out_numerator || !out_denominator) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Rational &length = to_cpp(footage)->get_length(); + *out_numerator = length.numerator(); + *out_denominator = length.denominator(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_proxy_enabled(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + return to_cpp(footage)->proxy_enabled() ? 1 : 0; +} + +int oaknode_footage_set_proxy_enabled(OakNodeFootage *footage, int enabled) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(footage)->set_proxy_enabled(enabled != 0); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_proxy_path(const OakNodeFootage *footage, char *buf, + int buf_size) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(footage)->proxy_path(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_proxy_state(const OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + return static_cast(to_cpp(footage)->proxy_state()); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_set_proxy(OakNodeFootage *footage, const char *path, + int state, int video_stream_index, + int preset_version, int enabled) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(footage)->set_proxy( + path ? path : "", + static_cast(state), + video_stream_index, preset_version, enabled != 0); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_footage_clear_proxy(OakNodeFootage *footage) +{ + if (!footage) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(footage)->clear_proxy(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/group.cpp b/src/node/c_api/group.cpp new file mode 100644 index 000000000..d49d6b34c --- /dev/null +++ b/src/node/c_api/group.cpp @@ -0,0 +1,294 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/group.h" + +#include "group/group.h" + +#include "valueconvert.h" + +namespace +{ + +inline olive::NodeGroup *to_group(OakNodeGroup *group) +{ + return reinterpret_cast(group); +} + +inline const olive::NodeGroup *to_group(const OakNodeGroup *group) +{ + return reinterpret_cast(group); +} + +inline olive::Node *to_node(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +inline OakNodeNode *from_node(olive::Node *node) +{ + return reinterpret_cast(node); +} + +} + +OakNodeGroup *oaknode_group_create(void) +{ + try { + olive::NodeGroup *group = new (std::nothrow) olive::NodeGroup(); + if (group) { + oaknode_c_api::alive_inc(); + } + return reinterpret_cast(group); + } catch (...) { + return NULL; + } +} + +OakNodeGroup *oaknode_group_cast(OakNodeNode *node) +{ + if (!node) { + return NULL; + } + + try { + return reinterpret_cast( + dynamic_cast(to_node(node))); + } catch (...) { + return NULL; + } +} + +void oaknode_group_free(OakNodeGroup *group) +{ + if (!group) { + return; + } + + try { + delete to_group(group); + oaknode_c_api::alive_dec(); + } catch (...) { + } +} + +int oaknode_group_add_input_passthrough(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, int element, + char *buf, int buf_size) +{ + if (!group || !node || !input_id) { + return OAKNODE_E_INVALID; + } + + try { + std::string id = to_group(group)->add_input_passthrough( + olive::NodeInput(to_node(node), input_id, element)); + return oaknode_c_api::copy_string(id, buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_add_input_passthrough_undoable(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, + int element, + OakUndoCommand **out_command) +{ + if (!group || !node || !input_id || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeGroupAddInputPassthrough( + to_group(group), + olive::NodeInput(to_node(node), input_id, element))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_remove_input_passthrough(OakNodeGroup *group, + OakNodeNode *node, + const char *input_id, int element) +{ + if (!group || !node || !input_id) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeInput input(to_node(node), input_id, element); + if (!to_group(group)->contains_input_passthrough(input)) { + return OAKNODE_E_NOT_FOUND; + } + to_group(group)->remove_input_passthrough(input); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_passthrough_count(const OakNodeGroup *group, int *out_count) +{ + if (!group || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + *out_count = + static_cast(to_group(group)->get_input_passthroughs().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_passthrough_id_at(const OakNodeGroup *group, int index, + char *buf, int buf_size) +{ + if (!group) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeGroup::InputPassthroughs &passthroughs = + to_group(group)->get_input_passthroughs(); + if (index < 0 || index >= static_cast(passthroughs.size())) { + return OAKNODE_E_NOT_FOUND; + } + return oaknode_c_api::copy_string(passthroughs[size_t(index)].first, buf, + buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_passthrough_input_at(const OakNodeGroup *group, int index, + OakNodeNode **out_node, char *buf, + int buf_size, int *out_element) +{ + if (!group) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeGroup::InputPassthroughs &passthroughs = + to_group(group)->get_input_passthroughs(); + if (index < 0 || index >= static_cast(passthroughs.size())) { + return OAKNODE_E_NOT_FOUND; + } + + const olive::NodeInput &input = passthroughs[size_t(index)].second; + if (out_node) { + *out_node = from_node(input.node()); + } + if (out_element) { + *out_element = input.element(); + } + return oaknode_c_api::copy_string(input.input(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_get_output_passthrough(const OakNodeGroup *group, + OakNodeNode **out_node) +{ + if (!group || !out_node) { + return OAKNODE_E_INVALID; + } + + try { + *out_node = from_node(to_group(group)->get_output_passthrough()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_set_output_passthrough(OakNodeGroup *group, + OakNodeNode *node) +{ + if (!group) { + return OAKNODE_E_INVALID; + } + + try { + to_group(group)->set_output_passthrough(to_node(node)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_set_output_passthrough_undoable( + OakNodeGroup *group, OakNodeNode *node, OakUndoCommand **out_command) +{ + if (!group || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeGroupSetOutputPassthrough(to_group(group), + to_node(node))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_group_resolve_input(OakNodeNode *node, const char *input_id, + int element, OakNodeNode **out_node, + char *buf, int buf_size, int *out_element) +{ + if (!node || !input_id) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeInput resolved = olive::NodeGroup::resolve_input( + olive::NodeInput(to_node(node), input_id, element)); + if (!resolved.is_valid()) { + return OAKNODE_E_NOT_FOUND; + } + + if (out_node) { + *out_node = from_node(resolved.node()); + } + if (out_element) { + *out_element = resolved.element(); + } + return oaknode_c_api::copy_string(resolved.input(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/keyframe.cpp b/src/node/c_api/keyframe.cpp new file mode 100644 index 000000000..50bed3625 --- /dev/null +++ b/src/node/c_api/keyframe.cpp @@ -0,0 +1,588 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/keyframe.h" + +#include "keyframe.h" +#include "node.h" +#include "nodeundo.h" + +#include "valueconvert.h" + +namespace +{ + +inline olive::NodeKeyframe *to_key(OakNodeKeyframe *keyframe) +{ + return reinterpret_cast(keyframe); +} + +inline const olive::NodeKeyframe *to_key(const OakNodeKeyframe *keyframe) +{ + return reinterpret_cast(keyframe); +} + +inline olive::Node *to_node(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +inline OakNodeNode *from_node(olive::Node *node) +{ + return reinterpret_cast(node); +} + +/** + * @brief Convert an oaknode_keyframe_type to olive::NodeKeyframe::Type. + * The oaknode enum mirrors the olive ordinals exactly (invalid = -1, + * linear = 0, hold = 1, bezier = 2). + */ +bool keyframe_type_from_oak(int type, olive::NodeKeyframe::Type *out) +{ + if (type < OAKNODE_KEYFRAME_LINEAR || type > OAKNODE_KEYFRAME_BEZIER) { + return false; + } + *out = static_cast(type); + return true; +} + +/** + * @brief Undoable set-type command (no olive command class exists for + * this; defined locally, mirroring NodeOverrideColorCommand's + * capture-on-redo pattern). + */ +class KeyframeSetTypeCommand : public olive::UndoCommand { +public: + KeyframeSetTypeCommand(olive::NodeKeyframe *key, + olive::NodeKeyframe::Type type) + : key_(key) + , new_type_(type) + , old_type_(olive::NodeKeyframe::k_invalid) + { + } + +protected: + virtual void redo() override + { + old_type_ = key_->type(); + key_->set_type(new_type_); + } + + virtual void undo() override + { + key_->set_type(old_type_); + } + +private: + olive::NodeKeyframe *key_; + olive::NodeKeyframe::Type new_type_; + olive::NodeKeyframe::Type old_type_; +}; + +/** + * @brief Undoable set-bezier-control command (no olive command class + * exists for this; defined locally). + */ +class KeyframeSetBezierControlCommand : public olive::UndoCommand { +public: + KeyframeSetBezierControlCommand(olive::NodeKeyframe *key, + olive::NodeKeyframe::BezierType handle, + const olive::PointF &point) + : key_(key) + , handle_(handle) + , new_point_(point) + { + } + +protected: + virtual void redo() override + { + old_point_ = key_->bezier_control(handle_); + key_->set_bezier_control(handle_, new_point_); + } + + virtual void undo() override + { + key_->set_bezier_control(handle_, old_point_); + } + +private: + olive::NodeKeyframe *key_; + olive::NodeKeyframe::BezierType handle_; + olive::PointF new_point_; + olive::PointF old_point_; +}; + +} + +OakNodeKeyframe *oaknode_keyframe_create(int64_t time_num, int64_t time_den, + const oaknode_value *value, int type, + int track, int element, + const char *input_id, + OakNodeNode *parent_or_null) +{ + olive::NodeKeyframe::Type keyframe_type; + if (!keyframe_type_from_oak(type, &keyframe_type)) { + return NULL; + } + + try { + olive::Variant variant; + if (value) { + if (!oaknode_c_api::variant_from_value(value, &variant)) { + return NULL; + } + } + + olive::core::Rational time(static_cast(time_num), + static_cast(time_den)); + olive::NodeKeyframe *key = new (std::nothrow) olive::NodeKeyframe( + time, variant, keyframe_type, track, element, + input_id ? input_id : "", to_node(parent_or_null)); + if (key) { + oaknode_c_api::alive_inc(); + } + return reinterpret_cast(key); + } catch (...) { + return NULL; + } +} + +void oaknode_keyframe_free(OakNodeKeyframe *keyframe) +{ + if (!keyframe) { + return; + } + + try { + delete to_key(keyframe); + oaknode_c_api::alive_dec(); + } catch (...) { + } +} + +int oaknode_keyframe_get_time(const OakNodeKeyframe *keyframe, + int64_t *out_num, int64_t *out_den) +{ + if (!keyframe || !out_num || !out_den) { + return OAKNODE_E_INVALID; + } + + try { + const olive::core::Rational &time = to_key(keyframe)->time(); + *out_num = time.numerator(); + *out_den = time.denominator(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_time(OakNodeKeyframe *keyframe, int64_t time_num, + int64_t time_den) +{ + if (!keyframe) { + return OAKNODE_E_INVALID; + } + + try { + to_key(keyframe)->set_time(olive::core::Rational( + static_cast(time_num), static_cast(time_den))); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_time_undoable(OakNodeKeyframe *keyframe, + int64_t time_num, int64_t time_den, + OakUndoCommand **out_command) +{ + if (!keyframe || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetKeyframeTimeCommand( + to_key(keyframe), + olive::core::Rational(static_cast(time_num), + static_cast(time_den)))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_value(const OakNodeKeyframe *keyframe, + oaknode_value *out) +{ + if (!keyframe || !out) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeKeyframe *key = to_key(keyframe); + const olive::Variant &variant = key->value(); + + // Preferred path: the parent node's declared input type pins the + // mapping. + olive::Node *parent = key->parent(); + if (parent && !key->input().empty() && + parent->has_input_with_id(key->input())) { + return oaknode_c_api::value_from_variant( + parent->get_input_data_type(key->input()), variant, out); + } + + // Orphan fallback: infer the POD type from the stored variant + // content. Numeric kinds are reported as FLOAT (the Variant kind is + // not recoverable across the POD). + if (variant.can_convert()) { + olive::core::Rational r = variant.value(); + *out = oaknode_value(); + out->type = OAKNODE_VALUE_RATIONAL; + out->num = r.numerator(); + out->den = r.denominator(); + return OAKNODE_OK; + } + if (variant.can_convert()) { + return oaknode_c_api::value_from_variant(olive::NodeValue::k_color, + variant, out); + } + if (variant.can_convert()) { + return oaknode_c_api::value_from_variant(olive::NodeValue::k_vec2, + variant, out); + } + if (variant.can_convert()) { + return oaknode_c_api::value_from_variant(olive::NodeValue::k_vec3, + variant, out); + } + if (variant.can_convert()) { + return oaknode_c_api::value_from_variant(olive::NodeValue::k_vec4, + variant, out); + } + if (variant.can_convert()) { + *out = oaknode_value(); + out->type = OAKNODE_VALUE_FLOAT; + out->f[0] = variant.to_double(); + return OAKNODE_OK; + } + + return OAKNODE_E_FAILED; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_value(OakNodeKeyframe *keyframe, + const oaknode_value *v) +{ + if (!keyframe || !v) { + return OAKNODE_E_INVALID; + } + + try { + olive::Variant variant; + if (!oaknode_c_api::variant_from_value(v, &variant)) { + return OAKNODE_E_INVALID; + } + to_key(keyframe)->set_value(variant); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_value_undoable(OakNodeKeyframe *keyframe, + const oaknode_value *v, + OakUndoCommand **out_command) +{ + if (!keyframe || !v || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + olive::Variant variant; + if (!oaknode_c_api::variant_from_value(v, &variant)) { + return OAKNODE_E_INVALID; + } + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetKeyframeValueCommand(to_key(keyframe), + variant)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_value_string(const OakNodeKeyframe *keyframe, + char *buf, int buf_size) +{ + if (!keyframe) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string(to_key(keyframe)->value().to_string(), + buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_value_string(OakNodeKeyframe *keyframe, + const char *value) +{ + if (!keyframe || !value) { + return OAKNODE_E_INVALID; + } + + try { + to_key(keyframe)->set_value(olive::Variant(value)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_value_string_undoable(OakNodeKeyframe *keyframe, + const char *value, + OakUndoCommand **out_command) +{ + if (!keyframe || !value || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetKeyframeValueCommand( + to_key(keyframe), olive::Variant(value))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_type(const OakNodeKeyframe *keyframe, int *out_type) +{ + if (!keyframe || !out_type) { + return OAKNODE_E_INVALID; + } + + try { + *out_type = static_cast(to_key(keyframe)->type()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_type(OakNodeKeyframe *keyframe, int type) +{ + if (!keyframe) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeKeyframe::Type keyframe_type; + if (!keyframe_type_from_oak(type, &keyframe_type)) { + return OAKNODE_E_INVALID; + } + to_key(keyframe)->set_type(keyframe_type); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_type_undoable(OakNodeKeyframe *keyframe, int type, + OakUndoCommand **out_command) +{ + if (!keyframe || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeKeyframe::Type keyframe_type; + if (!keyframe_type_from_oak(type, &keyframe_type)) { + return OAKNODE_E_INVALID; + } + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new KeyframeSetTypeCommand(to_key(keyframe), keyframe_type)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_bezier_control(const OakNodeKeyframe *keyframe, + int handle, double *out_x, + double *out_y) +{ + if (!keyframe || !out_x || !out_y) { + return OAKNODE_E_INVALID; + } + + try { + olive::PointF point; + if (handle == OAKNODE_KEYFRAME_IN_HANDLE) { + point = to_key(keyframe)->bezier_control_in(); + } else if (handle == OAKNODE_KEYFRAME_OUT_HANDLE) { + point = to_key(keyframe)->bezier_control_out(); + } else { + return OAKNODE_E_INVALID; + } + *out_x = point.x(); + *out_y = point.y(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_bezier_control(OakNodeKeyframe *keyframe, int handle, + double x, double y) +{ + if (!keyframe) { + return OAKNODE_E_INVALID; + } + + try { + if (handle == OAKNODE_KEYFRAME_IN_HANDLE) { + to_key(keyframe)->set_bezier_control_in(olive::PointF(x, y)); + } else if (handle == OAKNODE_KEYFRAME_OUT_HANDLE) { + to_key(keyframe)->set_bezier_control_out(olive::PointF(x, y)); + } else { + return OAKNODE_E_INVALID; + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_set_bezier_control_undoable(OakNodeKeyframe *keyframe, + int handle, double x, double y, + OakUndoCommand **out_command) +{ + if (!keyframe || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeKeyframe::BezierType bezier_handle; + if (handle == OAKNODE_KEYFRAME_IN_HANDLE) { + bezier_handle = olive::NodeKeyframe::k_in_handle; + } else if (handle == OAKNODE_KEYFRAME_OUT_HANDLE) { + bezier_handle = olive::NodeKeyframe::k_out_handle; + } else { + return OAKNODE_E_INVALID; + } + + OakUndoCommand *handle_ptr = oaknode_c_api::wrap_command( + new KeyframeSetBezierControlCommand(to_key(keyframe), bezier_handle, + olive::PointF(x, y))); + if (!handle_ptr) { + return OAKNODE_E_NOMEM; + } + *out_command = handle_ptr; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_track(const OakNodeKeyframe *keyframe, + int *out_track) +{ + if (!keyframe || !out_track) { + return OAKNODE_E_INVALID; + } + + try { + *out_track = to_key(keyframe)->track(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_element(const OakNodeKeyframe *keyframe, + int *out_element) +{ + if (!keyframe || !out_element) { + return OAKNODE_E_INVALID; + } + + try { + *out_element = to_key(keyframe)->element(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_input(const OakNodeKeyframe *keyframe, char *buf, + int buf_size) +{ + if (!keyframe) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string(to_key(keyframe)->input(), buf, + buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_keyframe_get_parent(const OakNodeKeyframe *keyframe, + OakNodeNode **out_node) +{ + if (!keyframe || !out_node) { + return OAKNODE_E_INVALID; + } + + try { + *out_node = from_node(to_key(keyframe)->parent()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/node.cpp b/src/node/c_api/node.cpp new file mode 100644 index 000000000..3af82b464 --- /dev/null +++ b/src/node/c_api/node.cpp @@ -0,0 +1,1030 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/node.h" + +#include +#include + +#include "nodeundo.h" + +#include "valueconvert.h" + +namespace +{ + +inline olive::Node *to_node(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +inline const olive::Node *to_node(const OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +inline OakNodeNode *from_node(olive::Node *node) +{ + return reinterpret_cast(node); +} + +std::atomic g_alive_count(0); + +/** + * @brief Validate that `input_id` names an existing input of `node`. + */ +bool has_input(const olive::Node *node, const char *input_id) +{ + return node && input_id && node->has_input_with_id(input_id); +} + +/** + * @brief Map a POD value for setting on `input_id`, validating that the + * POD type matches the input's declared type. + */ +int variant_for_input(olive::Node *node, const char *input_id, + const oaknode_value *v, olive::Variant *out) +{ + if (!v) { + return OAKNODE_E_INVALID; + } + + olive::NodeValue::Type type = node->get_input_data_type(input_id); + + if (oaknode_c_api::value_type_is_string(type) || + v->type != oaknode_c_api::value_type_to_oak(type)) { + return OAKNODE_E_INVALID; + } + + if (!oaknode_c_api::variant_from_value(v, out)) { + return OAKNODE_E_INVALID; + } + + return OAKNODE_OK; +} + +} + +namespace oaknode_c_api +{ + +void alive_inc() +{ + g_alive_count.fetch_add(1, std::memory_order_relaxed); +} + +void alive_dec() +{ + g_alive_count.fetch_sub(1, std::memory_order_relaxed); +} + +} + +int oaknode_debug_alive_count(void) +{ + return g_alive_count.load(std::memory_order_relaxed); +} + +/* ---- Metadata --------------------------------------------------------- */ + +int oaknode_node_get_id(const OakNodeNode *node, char *buf, int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string(to_node(node)->id(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_name(const OakNodeNode *node, char *buf, int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string(to_node(node)->name(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_label(const OakNodeNode *node, char *buf, int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + return oaknode_c_api::copy_string(to_node(node)->get_label(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_label(OakNodeNode *node, const char *label) +{ + if (!node || !label) { + return OAKNODE_E_INVALID; + } + + try { + to_node(node)->set_label(label); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_label_undoable(OakNodeNode *node, const char *label, + OakUndoCommand **out_command) +{ + if (!node || !label || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeRenameCommand(to_node(node), label)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_override_color(const OakNodeNode *node, int *out_value) +{ + if (!node || !out_value) { + return OAKNODE_E_INVALID; + } + + try { + *out_value = to_node(node)->get_override_color(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_override_color(OakNodeNode *node, int index) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + to_node(node)->set_override_color(index); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_override_color_undoable(OakNodeNode *node, int index, + OakUndoCommand **out_command) +{ + if (!node || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeOverrideColorCommand(to_node(node), index)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_is_enabled(const OakNodeNode *node, int *out_value) +{ + if (!node || !out_value) { + return OAKNODE_E_INVALID; + } + + try { + *out_value = to_node(node) + ->get_standard_value(olive::Node::k_enabled_input) + .to_bool() + ? 1 + : 0; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_enabled(OakNodeNode *node, int enabled) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + to_node(node)->set_standard_value(olive::Node::k_enabled_input, + olive::Variant(enabled != 0)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_enabled_undoable(OakNodeNode *node, int enabled, + OakUndoCommand **out_command) +{ + if (!node || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + olive::NodeInput ref(to_node(node), olive::Node::k_enabled_input); + olive::SplitValue split = + olive::NodeValue::split_normal_value_into_track_values( + olive::NodeValue::k_boolean, olive::Variant(enabled != 0)); + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetSplitStandardValueCommand(ref, split)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Input introspection ------------------------------------------------ */ + +int oaknode_node_input_count(const OakNodeNode *node, int *out_count) +{ + if (!node || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + *out_count = static_cast(to_node(node)->inputs().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_input_id(const OakNodeNode *node, int index, char *buf, + int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + const std::vector &inputs = to_node(node)->inputs(); + if (index < 0 || index >= static_cast(inputs.size())) { + return OAKNODE_E_NOT_FOUND; + } + return oaknode_c_api::copy_string(inputs[size_t(index)], buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_input_get_type(const OakNodeNode *node, const char *input_id, + int *out_type) +{ + if (!node || !out_type) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + *out_type = oaknode_c_api::value_type_to_oak( + to_node(node)->get_input_data_type(input_id)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_input_is_connected(const OakNodeNode *node, + const char *input_id, int *out_value) +{ + if (!node || !out_value) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + *out_value = to_node(node)->is_input_connected(input_id) ? 1 : 0; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_input_is_connectable(const OakNodeNode *node, + const char *input_id, int *out_value) +{ + if (!node || !out_value) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + *out_value = to_node(node)->is_input_connectable(input_id) ? 1 : 0; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_input_name(const OakNodeNode *node, const char *input_id, + char *buf, int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + return oaknode_c_api::copy_string(to_node(node)->get_input_name(input_id), + buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_input_get_connected_node(const OakNodeNode *node, + const char *input_id, + OakNodeNode **out_node) +{ + if (!node || !out_node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + *out_node = from_node(to_node(node)->get_connected_output(input_id)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Parameter access ----------------------------------------------------- */ + +int oaknode_node_get_input(const OakNodeNode *node, const char *input_id, + oaknode_value *out) +{ + if (!node || !out) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + olive::NodeValue::Type type = to_node(node)->get_input_data_type(input_id); + return oaknode_c_api::value_from_variant( + type, to_node(node)->get_standard_value(input_id), out); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_input(OakNodeNode *node, const char *input_id, + const oaknode_value *v) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + olive::Variant variant; + int rc = variant_for_input(to_node(node), input_id, v, &variant); + if (rc != OAKNODE_OK) { + return rc; + } + + to_node(node)->set_standard_value(input_id, variant); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_input_undoable(OakNodeNode *node, const char *input_id, + const oaknode_value *v, + OakUndoCommand **out_command) +{ + if (!node || !out_command) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + olive::Variant variant; + int rc = variant_for_input(to_node(node), input_id, v, &variant); + if (rc != OAKNODE_OK) { + return rc; + } + + olive::NodeValue::Type type = to_node(node)->get_input_data_type(input_id); + olive::NodeInput ref(to_node(node), input_id); + olive::SplitValue split = + olive::NodeValue::split_normal_value_into_track_values(type, variant); + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetSplitStandardValueCommand(ref, split)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_input_string(const OakNodeNode *node, + const char *input_id, char *buf, + int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + if (!oaknode_c_api::value_type_is_string( + to_node(node)->get_input_data_type(input_id))) { + return OAKNODE_E_INVALID; + } + return oaknode_c_api::copy_string( + to_node(node)->get_standard_value(input_id).to_string(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_input_string(OakNodeNode *node, const char *input_id, + const char *value) +{ + if (!node || !value) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + if (!oaknode_c_api::value_type_is_string( + to_node(node)->get_input_data_type(input_id))) { + return OAKNODE_E_INVALID; + } + to_node(node)->set_standard_value(input_id, olive::Variant(value)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_input_string_undoable(OakNodeNode *node, + const char *input_id, + const char *value, + OakUndoCommand **out_command) +{ + if (!node || !value || !out_command) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + if (!oaknode_c_api::value_type_is_string( + to_node(node)->get_input_data_type(input_id))) { + return OAKNODE_E_INVALID; + } + + olive::NodeInput ref(to_node(node), input_id); + olive::SplitValue split = + olive::NodeValue::split_normal_value_into_track_values( + to_node(node)->get_input_data_type(input_id), + olive::Variant(value)); + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeParamSetSplitStandardValueCommand(ref, split)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Graph editing -------------------------------------------------------- */ + +int oaknode_node_connect(OakNodeNode *output_node, OakNodeNode *input_node, + const char *input_id) +{ + if (!output_node || !input_node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(input_node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + if (!to_node(input_node)->is_input_connectable(input_id)) { + return OAKNODE_E_INVALID; + } + if (to_node(input_node)->is_input_connected(input_id) || + to_node(input_node)->parent() != to_node(output_node)->parent()) { + return OAKNODE_E_STATE; + } + + olive::Node::connect_edge(to_node(output_node), + olive::NodeInput(to_node(input_node), input_id)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_connect_undoable(OakNodeNode *output_node, + OakNodeNode *input_node, + const char *input_id, + OakUndoCommand **out_command) +{ + if (!output_node || !input_node || !out_command) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(input_node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + if (!to_node(input_node)->is_input_connectable(input_id)) { + return OAKNODE_E_INVALID; + } + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeEdgeAddCommand( + to_node(output_node), + olive::NodeInput(to_node(input_node), input_id))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_disconnect(OakNodeNode *input_node, const char *input_id) +{ + if (!input_node) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(input_node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + olive::Node *output = to_node(input_node)->get_connected_output(input_id); + if (!output) { + return OAKNODE_E_NOT_FOUND; + } + + olive::Node::disconnect_edge( + output, olive::NodeInput(to_node(input_node), input_id)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_disconnect_undoable(OakNodeNode *input_node, + const char *input_id, + OakUndoCommand **out_command) +{ + if (!input_node || !out_command) { + return OAKNODE_E_INVALID; + } + if (!has_input(to_node(input_node), input_id)) { + return OAKNODE_E_NOT_FOUND; + } + + try { + olive::Node *output = to_node(input_node)->get_connected_output(input_id); + if (!output) { + return OAKNODE_E_NOT_FOUND; + } + + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeEdgeRemoveCommand( + output, olive::NodeInput(to_node(input_node), input_id))); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_output_connection_count(const OakNodeNode *node, + int *out_count) +{ + if (!node || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + *out_count = static_cast(to_node(node)->output_connections().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_output_connection_node_at(const OakNodeNode *node, int index, + OakNodeNode **out_node) +{ + if (!node || !out_node) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Node::OutputConnections &connections = + to_node(node)->output_connections(); + if (index < 0 || index >= static_cast(connections.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out_node = from_node(connections[size_t(index)].second.node()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_output_connection_input_id_at(const OakNodeNode *node, + int index, char *buf, + int buf_size) +{ + if (!node) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Node::OutputConnections &connections = + to_node(node)->output_connections(); + if (index < 0 || index >= static_cast(connections.size())) { + return OAKNODE_E_NOT_FOUND; + } + return oaknode_c_api::copy_string( + connections[size_t(index)].second.input(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_output_connection_element_at(const OakNodeNode *node, + int index, int *out_element) +{ + if (!node || !out_element) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Node::OutputConnections &connections = + to_node(node)->output_connections(); + if (index < 0 || index >= static_cast(connections.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out_element = connections[size_t(index)].second.element(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Links --------------------------------------------------------------- */ + +int oaknode_node_link(OakNodeNode *a, OakNodeNode *b, int *out_linked) +{ + if (!a || !b) { + return OAKNODE_E_INVALID; + } + + try { + bool linked = olive::Node::link(to_node(a), to_node(b)); + if (out_linked) { + *out_linked = linked ? 1 : 0; + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_unlink(OakNodeNode *a, OakNodeNode *b, int *out_unlinked) +{ + if (!a || !b) { + return OAKNODE_E_INVALID; + } + + try { + bool unlinked = olive::Node::unlink(to_node(a), to_node(b)); + if (out_unlinked) { + *out_unlinked = unlinked ? 1 : 0; + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_link_undoable(OakNodeNode *a, OakNodeNode *b, int link, + OakUndoCommand **out_command) +{ + if (!a || !b || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeLinkCommand(to_node(a), to_node(b), link != 0)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_are_linked(const OakNodeNode *a, const OakNodeNode *b, + int *out_value) +{ + if (!a || !b || !out_value) { + return OAKNODE_E_INVALID; + } + + try { + *out_value = olive::Node::are_linked( + const_cast(to_node(a)), + const_cast(to_node(b))) + ? 1 + : 0; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_link_count(const OakNodeNode *node, int *out_count) +{ + if (!node || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + *out_count = static_cast(to_node(node)->links().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_link_at(const OakNodeNode *node, int index, + OakNodeNode **out_node) +{ + if (!node || !out_node) { + return OAKNODE_E_INVALID; + } + + try { + const std::vector &links = to_node(node)->links(); + if (index < 0 || index >= static_cast(links.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out_node = from_node(links[size_t(index)]); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Context positions ---------------------------------------------------- */ + +int oaknode_node_context_count(const OakNodeNode *node, int *out_count) +{ + if (!node || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + *out_count = static_cast(to_node(node)->get_context_positions().size()); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_context_node_at(const OakNodeNode *node, int index, + OakNodeNode **out_node) +{ + if (!node || !out_node) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Node::PositionMap &positions = + to_node(node)->get_context_positions(); + if (index < 0 || index >= static_cast(positions.size())) { + return OAKNODE_E_NOT_FOUND; + } + auto it = positions.cbegin(); + std::advance(it, index); + *out_node = from_node(it->first); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_get_context_position(const OakNodeNode *node, + OakNodeNode *context, double *out_x, + double *out_y, int *out_expanded) +{ + if (!node || !context) { + return OAKNODE_E_INVALID; + } + + try { + const olive::Node *self = to_node(node); + if (!self->context_contains_node(to_node(context))) { + return OAKNODE_E_NOT_FOUND; + } + + olive::Node::Position position = + const_cast(self)->get_node_position_data_in_context( + to_node(context)); + if (out_x) { + *out_x = position.position.x(); + } + if (out_y) { + *out_y = position.position.y(); + } + if (out_expanded) { + *out_expanded = position.expanded ? 1 : 0; + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_context_position(OakNodeNode *node, OakNodeNode *context, + double x, double y, int expanded) +{ + if (!node || !context) { + return OAKNODE_E_INVALID; + } + + try { + olive::Node::Position position(olive::PointF(x, y), expanded != 0); + to_node(node)->set_node_position_in_context(to_node(context), position); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_set_context_position_undoable(OakNodeNode *node, + OakNodeNode *context, double x, + double y, int expanded, + OakUndoCommand **out_command) +{ + if (!node || !context || !out_command) { + return OAKNODE_E_INVALID; + } + + try { + olive::Node::Position position(olive::PointF(x, y), expanded != 0); + // NOTE: NodeSetPositionCommand's redo() calls + // context_->set_node_position_in_context(node_, pos_), and the + // position map lives on the positioned node keyed by the context, + // so the command's (node, context) arguments are swapped relative + // to this function's signature. + OakUndoCommand *handle = oaknode_c_api::wrap_command( + new olive::NodeSetPositionCommand(to_node(context), to_node(node), + position)); + if (!handle) { + return OAKNODE_E_NOMEM; + } + *out_command = handle; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_node_remove_from_context(OakNodeNode *node, OakNodeNode *context) +{ + if (!node || !context) { + return OAKNODE_E_INVALID; + } + + try { + if (!to_node(node)->remove_node_from_context(to_node(context))) { + return OAKNODE_E_NOT_FOUND; + } + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +/* ---- Lifetime --------------------------------------------------------------- */ + +OakNodeNode *oaknode_node_create_copy(const OakNodeNode *node) +{ + if (!node) { + return NULL; + } + + try { + olive::Node *copy = to_node(node)->copy(); + if (copy) { + oaknode_c_api::alive_inc(); + } + return from_node(copy); + } catch (...) { + return NULL; + } +} + +void oaknode_node_free(OakNodeNode *node) +{ + if (!node) { + return; + } + + try { + delete to_node(node); + oaknode_c_api::alive_dec(); + } catch (...) { + } +} diff --git a/src/node/c_api/project.cpp b/src/node/c_api/project.cpp new file mode 100644 index 000000000..7b8f802da --- /dev/null +++ b/src/node/c_api/project.cpp @@ -0,0 +1,373 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/project.h" + +#include +#include +#include +#include + +#include "../src/project.h" + +namespace +{ + +olive::Project *to_cpp(OakNodeProject *project) +{ + return reinterpret_cast(project); +} + +const olive::Project *to_cpp(const OakNodeProject *project) +{ + return reinterpret_cast(project); +} + +olive::Node *to_cpp(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +OakNodeNode *to_c(olive::Node *node) +{ + return reinterpret_cast(node); +} + +/** + * @brief Shared two-stage string getter. + * + * Returns the required buffer size in bytes (including the terminating + * NUL) as a non-negative value. + */ +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + + if (buf && buf_size > 0) { + size_t copy_len = value.size(); + if (copy_len > static_cast(buf_size) - 1) { + copy_len = static_cast(buf_size) - 1; + } + memcpy(buf, value.data(), copy_len); + buf[copy_len] = '\0'; + } + + return required; +} + +} // namespace + +OakNodeProject *oaknode_project_init(void) +{ + try { + return reinterpret_cast( + new (std::nothrow) olive::Project()); + } catch (...) { + return NULL; + } +} + +void oaknode_project_free(OakNodeProject *project) +{ + delete to_cpp(project); +} + +int oaknode_project_initialize(OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + if (to_cpp(project)->root()) { + return OAKNODE_E_STATE; + } + to_cpp(project)->initialize(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_clear(OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->clear(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeFolder *oaknode_project_root(OakNodeProject *project) +{ + if (!project) { + return NULL; + } + + try { + return reinterpret_cast(to_cpp(project)->root()); + } catch (...) { + return NULL; + } +} + +int oaknode_project_name(const OakNodeProject *project, char *buf, int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->name(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_filename(const OakNodeProject *project, char *buf, + int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->filename(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_pretty_filename(const OakNodeProject *project, char *buf, + int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->pretty_filename(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_set_filename(OakNodeProject *project, const char *filename) +{ + if (!project || !filename) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->set_filename(filename); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_is_modified(const OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + return to_cpp(project)->is_modified() ? 1 : 0; +} + +int oaknode_project_set_modified(OakNodeProject *project, int modified) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->set_modified(modified != 0); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_is_new(const OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + return to_cpp(project)->is_new() ? 1 : 0; +} + +int oaknode_project_cache_path(const OakNodeProject *project, char *buf, + int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->cache_path(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_get_cache_location_setting(const OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return static_cast(to_cpp(project)->get_cache_location_setting()); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_set_cache_location_setting(OakNodeProject *project, + int setting) +{ + if (!project || setting < 0 || + setting > static_cast(olive::Project::k_cache_custom_path)) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->set_cache_location_setting( + static_cast(setting)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_get_custom_cache_path(const OakNodeProject *project, + char *buf, int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->get_custom_cache_path(), buf, + buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_set_custom_cache_path(OakNodeProject *project, + const char *path) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->set_custom_cache_path(path ? path : ""); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_get_uuid(const OakNodeProject *project, char *buf, + int buf_size) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return copy_string(to_cpp(project)->get_uuid(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_add_node(OakNodeProject *project, OakNodeNode *node) +{ + if (!project || !node) { + return OAKNODE_E_INVALID; + } + + try { + to_cpp(project)->add_node(to_cpp(node)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_remove_node(OakNodeProject *project, OakNodeNode *node) +{ + if (!project || !node) { + return OAKNODE_E_INVALID; + } + + try { + olive::Project *p = to_cpp(project); + olive::Node *n = to_cpp(node); + const auto &nodes = p->nodes(); + if (std::find(nodes.begin(), nodes.end(), n) == nodes.end()) { + return OAKNODE_E_NOT_FOUND; + } + p->remove_node(n); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_project_node_count(const OakNodeProject *project) +{ + if (!project) { + return OAKNODE_E_INVALID; + } + + try { + return static_cast(to_cpp(project)->nodes().size()); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeNode *oaknode_project_node_at(const OakNodeProject *project, int index) +{ + if (!project || index < 0) { + return NULL; + } + + try { + const auto &nodes = to_cpp(project)->nodes(); + if (static_cast(index) >= nodes.size()) { + return NULL; + } + return to_c(nodes[static_cast(index)]); + } catch (...) { + return NULL; + } +} diff --git a/src/node/c_api/sequence.cpp b/src/node/c_api/sequence.cpp new file mode 100644 index 000000000..28611373e --- /dev/null +++ b/src/node/c_api/sequence.cpp @@ -0,0 +1,319 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/sequence.h" + +#include "alivecount.h" +#include "node/track.h" + +#include "globals.h" +#include "output/track/tracklist.h" +#include "project/sequence/sequence.h" +#include "videoparams.h" + +// oakcommon defines its handle as `struct OakCommonVideoParams { +// olive::VideoParams impl; }` (src/common/c_api/videoparams.cpp) without +// exporting the definition. Echoing the identical layout here is the only +// way to hand native VideoParams values across without a field-by-field +// copy; keep in sync with oakcommon (flagged in the family-C report). +struct OakCommonVideoParams { + olive::VideoParams impl; +}; + +namespace +{ + +olive::Sequence *impl(OakNodeSequence *h) +{ + return reinterpret_cast(h); +} + +int get_rational(const olive::core::Rational &r, int *numerator, + int *denominator) +{ + if (!numerator || !denominator) { + return OAKNODE_E_INVALID; + } + *numerator = r.numerator(); + *denominator = r.denominator(); + return OAKNODE_OK; +} + +bool valid_track_type(int type) +{ + return type >= OAKNODE_TRACK_TYPE_VIDEO && type < OAKNODE_TRACK_TYPE_COUNT; +} + +} // namespace + +OakNodeSequence *oaknode_sequence_create(void) +{ + try { + olive::Sequence *s = new olive::Sequence(); + oaknode_c_api::alive_inc(); + return reinterpret_cast(s); + } catch (...) { + return nullptr; + } +} + +void oaknode_sequence_free(OakNodeSequence *sequence) +{ + if (!sequence) { + return; + } + olive::Sequence *s = impl(sequence); + // ~Sequence() deletes the owned TrackLists + delete s; + oaknode_c_api::alive_dec(); +} + +int oaknode_sequence_get_track_list(OakNodeSequence *sequence, int type, + OakNodeTrackList **out) +{ + if (!sequence || !out) { + return OAKNODE_E_INVALID; + } + if (!valid_track_type(type)) { + return OAKNODE_E_NOT_FOUND; + } + *out = reinterpret_cast( + impl(sequence)->track_list(static_cast(type))); + return OAKNODE_OK; +} + +int oaknode_sequence_get_track_count(OakNodeSequence *sequence, int type, + int *count) +{ + if (!sequence || !count) { + return OAKNODE_E_INVALID; + } + if (!valid_track_type(type)) { + return OAKNODE_E_NOT_FOUND; + } + *count = impl(sequence) + ->track_list(static_cast(type)) + ->get_track_count(); + return OAKNODE_OK; +} + +int oaknode_sequence_get_track_at(OakNodeSequence *sequence, int type, + int index, OakNodeTrack **out) +{ + if (!sequence || !out || index < 0) { + return OAKNODE_E_INVALID; + } + if (!valid_track_type(type)) { + return OAKNODE_E_NOT_FOUND; + } + olive::TrackList *list = + impl(sequence)->track_list(static_cast(type)); + if (index >= list->get_track_count()) { + return OAKNODE_E_NOT_FOUND; + } + *out = reinterpret_cast(list->get_track_at(index)); + return OAKNODE_OK; +} + +int oaknode_sequence_get_all_track_count(OakNodeSequence *sequence, int *count) +{ + if (!sequence || !count) { + return OAKNODE_E_INVALID; + } + *count = int(impl(sequence)->get_tracks().size()); + return OAKNODE_OK; +} + +int oaknode_sequence_get_all_track_at(OakNodeSequence *sequence, int index, + OakNodeTrack **out) +{ + if (!sequence || !out || index < 0) { + return OAKNODE_E_INVALID; + } + const auto &tracks = impl(sequence)->get_tracks(); + if (index >= int(tracks.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out = reinterpret_cast(tracks.at(index)); + return OAKNODE_OK; +} + +int oaknode_sequence_get_playhead(OakNodeSequence *sequence, int *numerator, + int *denominator) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(sequence)->get_playhead(), numerator, denominator); +} + +int oaknode_sequence_set_playhead(OakNodeSequence *sequence, int numerator, + int denominator) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + try { + impl(sequence)->set_playhead(olive::core::Rational(numerator, + denominator)); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_sequence_get_length(OakNodeSequence *sequence, int *numerator, + int *denominator) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(sequence)->get_length(), numerator, denominator); +} + +int oaknode_sequence_get_video_length(OakNodeSequence *sequence, int *numerator, + int *denominator) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(sequence)->get_video_length(), numerator, + denominator); +} + +int oaknode_sequence_get_audio_length(OakNodeSequence *sequence, int *numerator, + int *denominator) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(sequence)->get_audio_length(), numerator, + denominator); +} + +int oaknode_sequence_verify_length(OakNodeSequence *sequence) +{ + if (!sequence) { + return OAKNODE_E_INVALID; + } + try { + impl(sequence)->verify_length(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +/* --------------------------------------------------- Video/audio params */ + +int oaknode_sequence_get_video_stream_count(OakNodeSequence *sequence, + int *count) +{ + if (!sequence || !count) { + return OAKNODE_E_INVALID; + } + *count = impl(sequence)->get_video_stream_count(); + return OAKNODE_OK; +} + +int oaknode_sequence_get_audio_stream_count(OakNodeSequence *sequence, + int *count) +{ + if (!sequence || !count) { + return OAKNODE_E_INVALID; + } + *count = impl(sequence)->get_audio_stream_count(); + return OAKNODE_OK; +} + +int oaknode_sequence_get_video_params(OakNodeSequence *sequence, int index, + OakCommonVideoParams **out) +{ + if (!sequence || !out || index < 0) { + return OAKNODE_E_INVALID; + } + if (index >= impl(sequence)->get_video_stream_count()) { + return OAKNODE_E_NOT_FOUND; + } + try { + *out = new OakCommonVideoParams{impl(sequence)->get_video_params(index)}; + } catch (...) { + return OAKNODE_E_NOMEM; + } + return OAKNODE_OK; +} + +int oaknode_sequence_set_video_params(OakNodeSequence *sequence, int index, + const OakCommonVideoParams *params) +{ + if (!sequence || !params || index < 0) { + return OAKNODE_E_INVALID; + } + if (index >= impl(sequence)->get_video_stream_count()) { + return OAKNODE_E_NOT_FOUND; + } + try { + impl(sequence)->set_video_params(params->impl, index); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_sequence_get_audio_params(OakNodeSequence *sequence, int index, + OakAudioParams **out) +{ + if (!sequence || !out || index < 0) { + return OAKNODE_E_INVALID; + } + if (index >= impl(sequence)->get_audio_stream_count()) { + return OAKNODE_E_NOT_FOUND; + } + OakAudioParams *copy = + oakcore_audioparams_copy(impl(sequence)->get_audio_params(index).handle()); + if (!copy) { + return OAKNODE_E_NOMEM; + } + *out = copy; + return OAKNODE_OK; +} + +int oaknode_sequence_set_audio_params(OakNodeSequence *sequence, int index, + const OakAudioParams *params) +{ + if (!sequence || !params || index < 0) { + return OAKNODE_E_INVALID; + } + if (index >= impl(sequence)->get_audio_stream_count()) { + return OAKNODE_E_NOT_FOUND; + } + OakAudioParams *copy = oakcore_audioparams_copy(params); + if (!copy) { + return OAKNODE_E_NOMEM; + } + try { + impl(sequence)->set_audio_params( + olive::core::AudioParams::from_handle(copy), index); + } catch (...) { + oakcore_audioparams_free(copy); + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} diff --git a/src/node/c_api/serializer.cpp b/src/node/c_api/serializer.cpp new file mode 100644 index 000000000..fa08e4c7c --- /dev/null +++ b/src/node/c_api/serializer.cpp @@ -0,0 +1,357 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/serializer.h" + +#include +#include +#include +#include + +#include "../src/factory.h" +#include "../src/project.h" +#include "../src/project/serializer/serializer.h" +#include "xmlutils.h" + +struct OakNodeSerializerSaveData { + olive::ProjectSerializer::SaveData impl; + + OakNodeSerializerSaveData(olive::ProjectSerializer::LoadType type, + olive::Project *project) + : impl(type, project) + { + } +}; + +struct OakNodeSerializerLoadData { + olive::ProjectSerializer::LoadData impl; +}; + +namespace +{ + +bool g_initialized = false; + +olive::Project *to_cpp(OakNodeProject *project) +{ + return reinterpret_cast(project); +} + +olive::Node *to_cpp(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +OakNodeNode *to_c(olive::Node *node) +{ + return reinterpret_cast(node); +} + +bool is_valid_load_type(int load_type) +{ + return load_type >= static_cast(olive::ProjectSerializer::k_project) && + load_type <= + static_cast(olive::ProjectSerializer::k_only_keyframes); +} + +/** + * @brief Shared two-stage string getter. + * + * Returns the required buffer size in bytes (including the terminating + * NUL) as a non-negative value. + */ +int copy_string(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + + if (buf && buf_size > 0) { + size_t copy_len = value.size(); + if (copy_len > static_cast(buf_size) - 1) { + copy_len = static_cast(buf_size) - 1; + } + memcpy(buf, value.data(), copy_len); + buf[copy_len] = '\0'; + } + + return required; +} + +} // namespace + +int oaknode_serializer_initialize(void) +{ + if (g_initialized) { + return OAKNODE_OK; + } + + try { + // The loaders instantiate nodes by id through the factory. + olive::NodeFactory::initialize(); + olive::ProjectSerializer::initialize(); + g_initialized = true; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +void oaknode_serializer_shutdown(void) +{ + if (!g_initialized) { + return; + } + + try { + olive::ProjectSerializer::destroy(); + olive::NodeFactory::destroy(); + } catch (...) { + } + g_initialized = false; +} + +OakNodeSerializerSaveData *oaknode_serializer_savedata_create( + int load_type, OakNodeProject *project) +{ + if (!is_valid_load_type(load_type)) { + return NULL; + } + + try { + return new (std::nothrow) OakNodeSerializerSaveData( + static_cast(load_type), + to_cpp(project)); + } catch (...) { + return NULL; + } +} + +void oaknode_serializer_savedata_free(OakNodeSerializerSaveData *save_data) +{ + delete save_data; +} + +int oaknode_serializer_savedata_set_nodes( + OakNodeSerializerSaveData *save_data, OakNodeNode *const *nodes, int count) +{ + if (!save_data || !nodes || count < 0) { + return OAKNODE_E_INVALID; + } + + try { + std::vector cpp_nodes; + cpp_nodes.reserve(static_cast(count)); + for (int i = 0; i < count; i++) { + if (!nodes[i]) { + return OAKNODE_E_INVALID; + } + cpp_nodes.push_back(to_cpp(nodes[i])); + } + + save_data->impl.set_only_serialize_nodes(cpp_nodes); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_serializer_savedata_set_property( + OakNodeSerializerSaveData *save_data, OakNodeNode *node, const char *key, + const char *value) +{ + if (!save_data || !node || !key || !value) { + return OAKNODE_E_INVALID; + } + + try { + olive::ProjectSerializer::SerializedProperties properties = + save_data->impl.get_properties(); + properties[to_cpp(node)][key] = value; + save_data->impl.set_properties(properties); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_serializer_save_to_xml(OakNodeSerializerSaveData *save_data, + char *buf, int buf_size) +{ + if (!save_data) { + return OAKNODE_E_INVALID; + } + if (!g_initialized) { + return OAKNODE_E_STATE; + } + + try { + olive::XmlStreamWriter writer; + olive::ProjectSerializer::Result result = + olive::ProjectSerializer::save(&writer, save_data->impl); + if (result != olive::ProjectSerializer::k_success) { + return OAKNODE_E_FAILED; + } + return copy_string(writer.output(), buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_serializer_load_from_xml(OakNodeProject *project, const char *xml, + int load_type, int *out_result, + OakNodeSerializerLoadData **out_load_data, + char *details_buf, int details_buf_size) +{ + if (!xml || !out_result || !is_valid_load_type(load_type)) { + return OAKNODE_E_INVALID; + } + if (!g_initialized) { + return OAKNODE_E_STATE; + } + + if (out_load_data) { + *out_load_data = NULL; + } + + try { + olive::XmlStreamReader reader(xml); + olive::ProjectSerializer::Result result = olive::ProjectSerializer::load( + to_cpp(project), &reader, + static_cast(load_type)); + + *out_result = static_cast(result.code()); + + if (details_buf && details_buf_size > 0) { + copy_string(result.get_details(), details_buf, details_buf_size); + } + + if (result == olive::ProjectSerializer::k_success && out_load_data) { + auto *load_data = new (std::nothrow) OakNodeSerializerLoadData(); + if (!load_data) { + return OAKNODE_E_NOMEM; + } + load_data->impl = result.get_load_data(); + *out_load_data = load_data; + } + + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +void oaknode_serializer_loaddata_free(OakNodeSerializerLoadData *load_data) +{ + delete load_data; +} + +int oaknode_serializer_loaddata_node_count( + const OakNodeSerializerLoadData *load_data) +{ + if (!load_data) { + return OAKNODE_E_INVALID; + } + + try { + return static_cast(load_data->impl.nodes.size()); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +OakNodeNode *oaknode_serializer_loaddata_node_at( + const OakNodeSerializerLoadData *load_data, int index) +{ + if (!load_data || index < 0 || + static_cast(index) >= load_data->impl.nodes.size()) { + return NULL; + } + + try { + return to_c(load_data->impl.nodes[static_cast(index)]); + } catch (...) { + return NULL; + } +} + +int oaknode_serializer_loaddata_get_property( + const OakNodeSerializerLoadData *load_data, OakNodeNode *node, + const char *key, char *buf, int buf_size) +{ + if (!load_data || !node || !key) { + return OAKNODE_E_INVALID; + } + + try { + auto node_it = load_data->impl.properties.find(to_cpp(node)); + if (node_it == load_data->impl.properties.end()) { + return OAKNODE_E_NOT_FOUND; + } + auto key_it = node_it->second.find(key); + if (key_it == node_it->second.end()) { + return OAKNODE_E_NOT_FOUND; + } + return copy_string(key_it->second, buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_serializer_loaddata_connection_count( + const OakNodeSerializerLoadData *load_data) +{ + if (!load_data) { + return OAKNODE_E_INVALID; + } + + try { + return static_cast(load_data->impl.promised_connections.size()); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_serializer_loaddata_connection_at( + const OakNodeSerializerLoadData *load_data, int index, + OakNodeNode **out_output_node, OakNodeNode **out_input_node, + char *input_id_buf, int input_id_buf_size, int *out_element) +{ + if (!load_data || !out_output_node || !out_input_node || !out_element) { + return OAKNODE_E_INVALID; + } + if (index < 0 || static_cast(index) >= + load_data->impl.promised_connections.size()) { + return OAKNODE_E_NOT_FOUND; + } + + try { + const olive::Node::OutputConnection &connection = + load_data->impl.promised_connections[static_cast(index)]; + *out_output_node = to_c(connection.first); + *out_input_node = to_c(connection.second.node()); + if (input_id_buf && input_id_buf_size > 0) { + copy_string(connection.second.input(), input_id_buf, + input_id_buf_size); + } + *out_element = connection.second.element(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/track.cpp b/src/node/c_api/track.cpp new file mode 100644 index 000000000..ab635b986 --- /dev/null +++ b/src/node/c_api/track.cpp @@ -0,0 +1,574 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/track.h" + +#include + +#include "alivecount.h" + +#include "block/block.h" +#include "output/track/track.h" +#include "output/track/tracklist.h" +#include "project/sequence/sequence.h" + +namespace +{ + +olive::Track *impl(OakNodeTrack *h) +{ + return reinterpret_cast(h); +} + +olive::TrackList *list_impl(OakNodeTrackList *h) +{ + return reinterpret_cast(h); +} + +olive::Block *block_impl(OakNodeBlock *h) +{ + return reinterpret_cast(h); +} + +OakNodeTrack *wrap(olive::Track *t) +{ + return reinterpret_cast(t); +} + +OakNodeBlock *wrap_block(olive::Block *b) +{ + return reinterpret_cast(b); +} + +bool valid_type(int type) +{ + return type >= OAKNODE_TRACK_TYPE_VIDEO && type < OAKNODE_TRACK_TYPE_COUNT; +} + +/** + * @brief Refresh the cached lengths after a block mutation + * + * De-Qt wave: TrackList::update_total_length / Sequence::verify_length + * were signal-driven; the C API performs the refresh synchronously so + * that state read back right after a mutation is consistent. + */ +void refresh_lengths(olive::Track *t) +{ + olive::Sequence *s = t->sequence(); + if (s && valid_type(int(t->type()))) { + olive::TrackList *l = s->track_list(t->type()); + if (l) { + l->update_total_length(); + } + s->verify_length(); + } +} + +int get_rational(const olive::core::Rational &r, int *numerator, + int *denominator) +{ + if (!numerator || !denominator) { + return OAKNODE_E_INVALID; + } + *numerator = r.numerator(); + *denominator = r.denominator(); + return OAKNODE_OK; +} + +} // namespace + +/* ---------------------------------------------------------------- Track */ + +OakNodeTrack *oaknode_track_create(int type) +{ + if (!valid_type(type)) { + return nullptr; + } + try { + olive::Track *t = new olive::Track(); + t->set_type(static_cast(type)); + oaknode_c_api::alive_inc(); + return wrap(t); + } catch (...) { + return nullptr; + } +} + +void oaknode_track_free(OakNodeTrack *track) +{ + if (!track) { + return; + } + delete impl(track); + oaknode_c_api::alive_dec(); +} + +int oaknode_track_get_type(OakNodeTrack *track, int *type) +{ + if (!track || !type) { + return OAKNODE_E_INVALID; + } + *type = int(impl(track)->type()); + return OAKNODE_OK; +} + +int oaknode_track_set_type(OakNodeTrack *track, int type) +{ + if (!track || !valid_type(type)) { + return OAKNODE_E_INVALID; + } + impl(track)->set_type(static_cast(type)); + return OAKNODE_OK; +} + +int oaknode_track_get_height(OakNodeTrack *track, double *height) +{ + if (!track || !height) { + return OAKNODE_E_INVALID; + } + *height = impl(track)->get_track_height(); + return OAKNODE_OK; +} + +int oaknode_track_set_height(OakNodeTrack *track, double height) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + impl(track)->set_track_height(height); + return OAKNODE_OK; +} + +int oaknode_track_get_height_in_pixels(OakNodeTrack *track, int *height) +{ + if (!track || !height) { + return OAKNODE_E_INVALID; + } + *height = impl(track)->get_track_height_in_pixels(); + return OAKNODE_OK; +} + +int oaknode_track_set_height_in_pixels(OakNodeTrack *track, int height) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + impl(track)->set_track_height_in_pixels(height); + return OAKNODE_OK; +} + +int oaknode_track_get_default_height_in_pixels(void) +{ + return olive::Track::get_default_track_height_in_pixels(); +} + +int oaknode_track_get_minimum_height_in_pixels(void) +{ + return olive::Track::get_minimum_track_height_in_pixels(); +} + +int oaknode_track_get_index(OakNodeTrack *track, int *index) +{ + if (!track || !index) { + return OAKNODE_E_INVALID; + } + *index = impl(track)->index(); + return OAKNODE_OK; +} + +int oaknode_track_set_index(OakNodeTrack *track, int index) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + impl(track)->set_index(index); + return OAKNODE_OK; +} + +int oaknode_track_get_muted(OakNodeTrack *track, int *muted) +{ + if (!track || !muted) { + return OAKNODE_E_INVALID; + } + *muted = impl(track)->is_muted() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_track_set_muted(OakNodeTrack *track, int muted) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + impl(track)->set_muted(muted != 0); + return OAKNODE_OK; +} + +int oaknode_track_get_locked(OakNodeTrack *track, int *locked) +{ + if (!track || !locked) { + return OAKNODE_E_INVALID; + } + *locked = impl(track)->is_locked() ? 1 : 0; + return OAKNODE_OK; +} + +int oaknode_track_set_locked(OakNodeTrack *track, int locked) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + impl(track)->set_locked(locked != 0); + return OAKNODE_OK; +} + +int oaknode_track_get_reference(OakNodeTrack *track, int *type, int *index) +{ + if (!track || !type || !index) { + return OAKNODE_E_INVALID; + } + olive::Track::Reference ref = impl(track)->to_reference(); + *type = int(ref.type()); + *index = ref.index(); + return OAKNODE_OK; +} + +int oaknode_track_get_length(OakNodeTrack *track, int *numerator, + int *denominator) +{ + if (!track) { + return OAKNODE_E_INVALID; + } + return get_rational(impl(track)->track_length(), numerator, denominator); +} + +int oaknode_track_get_sequence(OakNodeTrack *track, OakNodeSequence **out) +{ + if (!track || !out) { + return OAKNODE_E_INVALID; + } + *out = reinterpret_cast(impl(track)->sequence()); + return OAKNODE_OK; +} + +/* ------------------------------------------------------- Track blocks */ + +int oaknode_track_get_block_count(OakNodeTrack *track, int *count) +{ + if (!track || !count) { + return OAKNODE_E_INVALID; + } + *count = int(impl(track)->blocks().size()); + return OAKNODE_OK; +} + +int oaknode_track_get_block_at(OakNodeTrack *track, int index, + OakNodeBlock **out) +{ + if (!track || !out || index < 0) { + return OAKNODE_E_INVALID; + } + const auto &blocks = impl(track)->blocks(); + if (index >= int(blocks.size())) { + return OAKNODE_E_NOT_FOUND; + } + *out = wrap_block(blocks.at(index)); + return OAKNODE_OK; +} + +int oaknode_track_append_block(OakNodeTrack *track, OakNodeBlock *block) +{ + if (!track || !block) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->append_block(block_impl(block)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_prepend_block(OakNodeTrack *track, OakNodeBlock *block) +{ + if (!track || !block) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->prepend_block(block_impl(block)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_insert_block_at_index(OakNodeTrack *track, + OakNodeBlock *block, int index) +{ + if (!track || !block) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->insert_block_at_index(block_impl(block), index); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_insert_block_after(OakNodeTrack *track, OakNodeBlock *block, + OakNodeBlock *before) +{ + if (!track || !block || !before) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->insert_block_after(block_impl(block), block_impl(before)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_insert_block_before(OakNodeTrack *track, OakNodeBlock *block, + OakNodeBlock *after) +{ + if (!track || !block || !after) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->insert_block_before(block_impl(block), block_impl(after)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_ripple_remove_block(OakNodeTrack *track, OakNodeBlock *block) +{ + if (!track || !block) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->ripple_remove_block(block_impl(block)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_replace_block(OakNodeTrack *track, OakNodeBlock *old_block, + OakNodeBlock *new_block) +{ + if (!track || !old_block || !new_block) { + return OAKNODE_E_INVALID; + } + try { + impl(track)->replace_block(block_impl(old_block), block_impl(new_block)); + } catch (...) { + return OAKNODE_E_FAILED; + } + refresh_lengths(impl(track)); + return OAKNODE_OK; +} + +int oaknode_track_get_block_index(OakNodeTrack *track, OakNodeBlock *block, + int *index) +{ + if (!track || !block || !index) { + return OAKNODE_E_INVALID; + } + int i = impl(track)->get_array_index_from_block(block_impl(block)); + if (i < 0) { + return OAKNODE_E_NOT_FOUND; + } + *index = i; + return OAKNODE_OK; +} + +int oaknode_track_get_block_containing_time(OakNodeTrack *track, int numerator, + int denominator, + OakNodeBlock **out) +{ + if (!track || !out) { + return OAKNODE_E_INVALID; + } + olive::Block *b = impl(track)->block_containing_time( + olive::core::Rational(numerator, denominator)); + if (!b) { + return OAKNODE_E_NOT_FOUND; + } + *out = wrap_block(b); + return OAKNODE_OK; +} + +int oaknode_track_get_visible_block_at_time(OakNodeTrack *track, int numerator, + int denominator, + OakNodeBlock **out) +{ + if (!track || !out) { + return OAKNODE_E_INVALID; + } + olive::Block *b = impl(track)->visible_block_at_time( + olive::core::Rational(numerator, denominator)); + if (!b) { + return OAKNODE_E_NOT_FOUND; + } + *out = wrap_block(b); + return OAKNODE_OK; +} + +int oaknode_track_is_range_free(OakNodeTrack *track, int in_num, int in_den, + int out_num, int out_den, int *is_free) +{ + if (!track || !is_free) { + return OAKNODE_E_INVALID; + } + *is_free = impl(track)->is_range_free( + olive::core::TimeRange( + olive::core::Rational(in_num, in_den), + olive::core::Rational(out_num, out_den))) ? + 1 : + 0; + return OAKNODE_OK; +} + +/* ------------------------------------------------------------ TrackList */ + +int oaknode_tracklist_get_type(OakNodeTrackList *list, int *type) +{ + if (!list || !type) { + return OAKNODE_E_INVALID; + } + *type = int(list_impl(list)->type()); + return OAKNODE_OK; +} + +int oaknode_tracklist_get_track_count(OakNodeTrackList *list, int *count) +{ + if (!list || !count) { + return OAKNODE_E_INVALID; + } + *count = list_impl(list)->get_track_count(); + return OAKNODE_OK; +} + +int oaknode_tracklist_get_track_at(OakNodeTrackList *list, int index, + OakNodeTrack **out) +{ + if (!list || !out || index < 0) { + return OAKNODE_E_INVALID; + } + if (index >= list_impl(list)->get_track_count()) { + return OAKNODE_E_NOT_FOUND; + } + *out = wrap(list_impl(list)->get_track_at(index)); + return OAKNODE_OK; +} + +int oaknode_tracklist_get_total_length(OakNodeTrackList *list, int *numerator, + int *denominator) +{ + if (!list) { + return OAKNODE_E_INVALID; + } + return get_rational(list_impl(list)->get_total_length(), numerator, + denominator); +} + +int oaknode_tracklist_get_array_size(OakNodeTrackList *list, int *size) +{ + if (!list || !size) { + return OAKNODE_E_INVALID; + } + *size = list_impl(list)->array_size(); + return OAKNODE_OK; +} + +int oaknode_tracklist_add_track(OakNodeTrackList *list, OakNodeTrack *track) +{ + if (!list || !track) { + return OAKNODE_E_INVALID; + } + olive::TrackList *l = list_impl(list); + olive::Track *t = impl(track); + olive::Sequence *sequence = l->parent(); + if (!sequence) { + return OAKNODE_E_STATE; + } + try { + // Graph steps of TimelineAddTrackCommand::redo() minus auto-merge + t->set_parent(l->get_parent_graph()); + if (l->get_track_count() > 0) { + t->set_track_height( + l->get_track_at(l->get_track_count() - 1)->get_track_height()); + } + l->array_append(); + olive::Node::connect_edge(t, l->track_input(l->array_size() - 1)); + + // De-Qt wave: the former signal emissions are the caller's job + sequence->update_track_cache(); + l->update_total_length(); + sequence->verify_length(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} + +int oaknode_tracklist_remove_track(OakNodeTrackList *list, OakNodeTrack *track) +{ + if (!list || !track) { + return OAKNODE_E_INVALID; + } + olive::TrackList *l = list_impl(list); + olive::Track *t = impl(track); + olive::Sequence *sequence = l->parent(); + if (!sequence) { + return OAKNODE_E_STATE; + } + + const auto &tracks = l->get_tracks(); + auto it = std::find(tracks.begin(), tracks.end(), t); + if (it == tracks.end()) { + return OAKNODE_E_NOT_FOUND; + } + + try { + int cache_index = int(it - tracks.begin()); + int array_index = l->get_array_index_from_cache_index(cache_index); + + olive::Node::disconnect_edge(t, l->track_input(array_index)); + sequence->input_array_remove(l->track_input(), array_index); + + // De-Qt wave: the former signal emissions are the caller's job + sequence->update_track_cache(); + l->update_total_length(); + sequence->verify_length(); + } catch (...) { + return OAKNODE_E_FAILED; + } + return OAKNODE_OK; +} diff --git a/src/node/c_api/traverser.cpp b/src/node/c_api/traverser.cpp new file mode 100644 index 000000000..2db94ff9f --- /dev/null +++ b/src/node/c_api/traverser.cpp @@ -0,0 +1,236 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/traverser.h" + +#include + +#include "traverser.h" +#include "valuedatabase.h" + +#include "valueconvert.h" + +struct OakNodeValueDatabase { + olive::NodeValueDatabase impl; +}; + +namespace +{ + +inline olive::NodeTraverser *to_traverser(OakNodeTraverser *traverser) +{ + return reinterpret_cast(traverser); +} + +inline olive::Node *to_node(OakNodeNode *node) +{ + return reinterpret_cast(node); +} + +/** + * @brief Find the table named `key`, or NULL when absent. + */ +const olive::NodeValueTable *find_table(const OakNodeValueDatabase *db, + const char *key) +{ + for (auto it = db->impl.cbegin(); it != db->impl.cend(); ++it) { + if (it->first == key) { + return &it->second; + } + } + return nullptr; +} + +} + +OakNodeTraverser *oaknode_traverser_init(void) +{ + try { + olive::NodeTraverser *traverser = new (std::nothrow) olive::NodeTraverser(); + if (traverser) { + oaknode_c_api::alive_inc(); + } + return reinterpret_cast(traverser); + } catch (...) { + return NULL; + } +} + +void oaknode_traverser_free(OakNodeTraverser *traverser) +{ + if (!traverser) { + return; + } + + try { + delete to_traverser(traverser); + oaknode_c_api::alive_dec(); + } catch (...) { + } +} + +int oaknode_traverser_generate_database(OakNodeTraverser *traverser, + OakNodeNode *node, int64_t in_num, + int64_t in_den, int64_t out_num, + int64_t out_den, + OakNodeValueDatabase **out_db) +{ + if (!traverser || !node || !out_db) { + return OAKNODE_E_INVALID; + } + + try { + olive::core::Rational in(static_cast(in_num), + static_cast(in_den)); + olive::core::Rational out(static_cast(out_num), + static_cast(out_den)); + + OakNodeValueDatabase *db = new (std::nothrow) OakNodeValueDatabase(); + if (!db) { + return OAKNODE_E_NOMEM; + } + + db->impl = to_traverser(traverser)->generate_database( + to_node(node), olive::core::TimeRange(in, out)); + + *out_db = db; + oaknode_c_api::alive_inc(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +void oaknode_traverser_database_free(OakNodeValueDatabase *db) +{ + if (!db) { + return; + } + + delete db; + oaknode_c_api::alive_dec(); +} + +int oaknode_traverser_database_row_count(const OakNodeValueDatabase *db, + int *out_count) +{ + if (!db || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + int count = 0; + for (auto it = db->impl.cbegin(); it != db->impl.cend(); ++it) { + count++; + } + *out_count = count; + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_traverser_database_row_key_at(const OakNodeValueDatabase *db, + int index, char *buf, int buf_size) +{ + if (!db) { + return OAKNODE_E_INVALID; + } + + try { + if (index < 0) { + return OAKNODE_E_NOT_FOUND; + } + auto it = db->impl.cbegin(); + for (int i = 0; i < index && it != db->impl.cend(); i++, ++it) { + } + if (it == db->impl.cend()) { + return OAKNODE_E_NOT_FOUND; + } + return oaknode_c_api::copy_string(it->first, buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_traverser_database_row_value_count(const OakNodeValueDatabase *db, + const char *key, + int *out_count) +{ + if (!db || !key || !out_count) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeValueTable *table = find_table(db, key); + if (!table) { + return OAKNODE_E_NOT_FOUND; + } + *out_count = table->count(); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_traverser_database_value_at(const OakNodeValueDatabase *db, + const char *key, int index, + oaknode_value *out) +{ + if (!db || !key || !out) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeValueTable *table = find_table(db, key); + if (!table || index < 0 || index >= table->count()) { + return OAKNODE_E_NOT_FOUND; + } + + const olive::NodeValue &value = table->at(index); + return oaknode_c_api::value_from_variant(value.type(), value.data(), + out); + } catch (...) { + return OAKNODE_E_FAILED; + } +} + +int oaknode_traverser_database_value_string_at(const OakNodeValueDatabase *db, + const char *key, int index, + char *buf, int buf_size) +{ + if (!db || !key) { + return OAKNODE_E_INVALID; + } + + try { + const olive::NodeValueTable *table = find_table(db, key); + if (!table || index < 0 || index >= table->count()) { + return OAKNODE_E_NOT_FOUND; + } + + const olive::NodeValue &value = table->at(index); + return oaknode_c_api::copy_string( + olive::NodeValue::value_to_string(value.type(), value.data(), false), + buf, buf_size); + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/node/c_api/valueconvert.h b/src/node/c_api/valueconvert.h new file mode 100644 index 000000000..395997ad2 --- /dev/null +++ b/src/node/c_api/valueconvert.h @@ -0,0 +1,273 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODE_C_API_VALUECONVERT_H +#define OAK_NODE_C_API_VALUECONVERT_H + +// Internal helpers shared by the oaknode c_api translation units: +// oaknode_value <-> olive::Variant mapping, the pinned +// oaknode_value_type <-> olive::NodeValue::Type mapping, two-stage string +// copy, OakUndoCommand wrapping and the debug alive counter. + +#include "node/node.h" + +#include +#include +#include + +#include "value.h" + +// Internal layout of the OakUndoCommand handle, shared with the oakundo +// module (src/undo/c_api/commandhandle.h). Included so undoable variants +// can hand out owned handles wrapping freshly created olive commands. +#include "../../undo/c_api/commandhandle.h" + +namespace oaknode_c_api +{ + +/** + * @brief Bump/release the debug alive counter (defined in node.cpp). + */ +void alive_inc(); +void alive_dec(); + +/** + * @brief Shared two-stage string getter. + * + * Returns the required buffer size in bytes (including the terminating + * NUL) as a non-negative value. + */ +inline int copy_string(const std::string &value, char *buf, int buf_size) +{ + int required = static_cast(value.size()) + 1; + + if (buf && buf_size > 0) { + size_t copy_len = value.size(); + if (copy_len > static_cast(buf_size) - 1) { + copy_len = static_cast(buf_size) - 1; + } + memcpy(buf, value.data(), copy_len); + buf[copy_len] = '\0'; + } + + return required; +} + +/** + * @brief Pinned mapping olive::NodeValue::Type -> oaknode_value_type + * (see the table on oaknode_value_type in node/node.h). Types without a + * POD representation map to OAKNODE_VALUE_NONE. + */ +inline int value_type_to_oak(olive::NodeValue::Type type) +{ + switch (type) { + case olive::NodeValue::k_int: + return OAKNODE_VALUE_INT; + case olive::NodeValue::k_float: + return OAKNODE_VALUE_FLOAT; + case olive::NodeValue::k_boolean: + return OAKNODE_VALUE_BOOL; + case olive::NodeValue::k_rational: + return OAKNODE_VALUE_RATIONAL; + case olive::NodeValue::k_color: + return OAKNODE_VALUE_COLOR; + case olive::NodeValue::k_vec2: + return OAKNODE_VALUE_VEC2; + case olive::NodeValue::k_vec3: + return OAKNODE_VALUE_VEC3; + case olive::NodeValue::k_vec4: + return OAKNODE_VALUE_VEC4; + case olive::NodeValue::k_combo: + return OAKNODE_VALUE_COMBO; + case olive::NodeValue::k_file: + case olive::NodeValue::k_text: + case olive::NodeValue::k_font: + case olive::NodeValue::k_str_combo: + return OAKNODE_VALUE_STRING; + default: + return OAKNODE_VALUE_NONE; + } +} + +/** + * @brief 1 if the olive type is string-carried (no POD representation, + * handled by the dedicated string functions). + */ +inline bool value_type_is_string(olive::NodeValue::Type type) +{ + return type == olive::NodeValue::k_file || type == olive::NodeValue::k_text || + type == olive::NodeValue::k_font || type == olive::NodeValue::k_str_combo; +} + +/** + * @brief Build an olive::Variant from an oaknode_value POD. + * + * `value->type` must be one of the POD-carrying oaknode_value_type + * values (STRING is rejected: no string data fits the POD). + */ +inline bool variant_from_value(const oaknode_value *value, olive::Variant *out) +{ + using olive::core::Color; + using olive::core::Rational; + using olive::Vector2D; + using olive::Vector3D; + using olive::Vector4D; + + switch (value->type) { + case OAKNODE_VALUE_INT: + case OAKNODE_VALUE_COMBO: + *out = olive::Variant(value->num); + return true; + case OAKNODE_VALUE_FLOAT: + *out = olive::Variant(value->f[0]); + return true; + case OAKNODE_VALUE_BOOL: + *out = olive::Variant(value->num != 0); + return true; + case OAKNODE_VALUE_RATIONAL: + *out = olive::Variant::from_value( + Rational(static_cast(value->num), static_cast(value->den))); + return true; + case OAKNODE_VALUE_COLOR: + *out = olive::Variant::from_value( + Color(static_cast(value->f[0]), static_cast(value->f[1]), + static_cast(value->f[2]), static_cast(value->f[3]))); + return true; + case OAKNODE_VALUE_VEC2: + *out = olive::Variant::from_value(Vector2D(static_cast(value->f[0]), + static_cast(value->f[1]))); + return true; + case OAKNODE_VALUE_VEC3: + *out = olive::Variant::from_value(Vector3D(static_cast(value->f[0]), + static_cast(value->f[1]), + static_cast(value->f[2]))); + return true; + case OAKNODE_VALUE_VEC4: + *out = olive::Variant::from_value(Vector4D(static_cast(value->f[0]), + static_cast(value->f[1]), + static_cast(value->f[2]), + static_cast(value->f[3]))); + return true; + default: + return false; + } +} + +/** + * @brief Map an olive::Variant of declared type `type` into an + * oaknode_value POD. + * + * Returns OAKNODE_OK, OAKNODE_E_INVALID for string-family types (use the + * string getters), or OAKNODE_E_FAILED for types without a POD + * representation. + */ +inline int value_from_variant(olive::NodeValue::Type type, const olive::Variant &v, + oaknode_value *out) +{ + using olive::core::Color; + using olive::core::Rational; + using olive::Vector2D; + using olive::Vector3D; + using olive::Vector4D; + + if (value_type_is_string(type)) { + return OAKNODE_E_INVALID; + } + + *out = oaknode_value(); + out->type = value_type_to_oak(type); + + switch (type) { + case olive::NodeValue::k_none: + return OAKNODE_OK; + case olive::NodeValue::k_int: + case olive::NodeValue::k_combo: + out->num = v.to_long_long(); + return OAKNODE_OK; + case olive::NodeValue::k_float: + out->f[0] = v.to_double(); + return OAKNODE_OK; + case olive::NodeValue::k_boolean: + out->num = v.to_bool() ? 1 : 0; + return OAKNODE_OK; + case olive::NodeValue::k_rational: { + Rational r = v.value(); + out->num = r.numerator(); + out->den = r.denominator(); + return OAKNODE_OK; + } + case olive::NodeValue::k_color: { + Color c = v.value(); + out->f[0] = c.red(); + out->f[1] = c.green(); + out->f[2] = c.blue(); + out->f[3] = c.alpha(); + return OAKNODE_OK; + } + case olive::NodeValue::k_vec2: { + Vector2D vec = v.value(); + out->f[0] = vec.x(); + out->f[1] = vec.y(); + return OAKNODE_OK; + } + case olive::NodeValue::k_vec3: { + Vector3D vec = v.value(); + out->f[0] = vec.x(); + out->f[1] = vec.y(); + out->f[2] = vec.z(); + return OAKNODE_OK; + } + case olive::NodeValue::k_vec4: { + Vector4D vec = v.value(); + out->f[0] = vec.x(); + out->f[1] = vec.y(); + out->f[2] = vec.z(); + out->f[3] = vec.w(); + return OAKNODE_OK; + } + default: + out->type = OAKNODE_VALUE_NONE; + return OAKNODE_E_FAILED; + } +} + +/** + * @brief Wrap a freshly created olive::UndoCommand in an owned + * OakUndoCommand handle. Returns NULL on allocation failure. + */ +inline OakUndoCommand *wrap_command(olive::UndoCommand *command) +{ + if (!command) { + return NULL; + } + + OakUndoCommand *handle = new (std::nothrow) OakUndoCommand(); + if (!handle) { + delete command; + return NULL; + } + handle->command = command; + handle->owned = true; + return handle; +} + +} + +#endif // OAK_NODE_C_API_VALUECONVERT_H diff --git a/src/node/src/CMakeLists.txt b/src/node/src/CMakeLists.txt new file mode 100644 index 000000000..8a4d37088 --- /dev/null +++ b/src/node/src/CMakeLists.txt @@ -0,0 +1,36 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# Modifications Copyright (C) 2025 mikesolar +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# oaknode library. The tree is flat: src/node/src is the include root and all +# in-module includes use the unprefixed form ("value.h", "block/block.h"). +# The per-subdirectory CMakeLists.txt files are inert legacy (they only append +# to OLIVE_SOURCES) and are intentionally not added. +file(GLOB_RECURSE OAKNODE_SOURCES CONFIGURE_DEPENDS *.cpp) + +add_library(oaknode SHARED ${OAKNODE_SOURCES}) + +target_include_directories(oaknode PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${OAK_REPO_ROOT}/include + ${OAK_REPO_ROOT}/src/common/src + ${OAK_REPO_ROOT}/src/undo/src + ${OAK_REPO_ROOT}/core/include + ${OAK_REPO_ROOT}/ffmpeg_bridge/include + ${OAK_REPO_ROOT}/third_party/openfx/include + ${OCIO_INCLUDE_DIRS} + ${OIIO_INCLUDE_DIRS} +) diff --git a/src/node/src/audio/CMakeLists.txt b/src/node/src/audio/CMakeLists.txt new file mode 100644 index 000000000..a0e886b57 --- /dev/null +++ b/src/node/src/audio/CMakeLists.txt @@ -0,0 +1,23 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(pan) +add_subdirectory(volume) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/audio/pan/CMakeLists.txt b/src/node/src/audio/pan/CMakeLists.txt new file mode 100644 index 000000000..12dd5cce9 --- /dev/null +++ b/src/node/src/audio/pan/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/audio/pan/pan.h + node/audio/pan/pan.cpp + PARENT_SCOPE +) diff --git a/src/node/src/audio/pan/pan.cpp b/src/node/src/audio/pan/pan.cpp new file mode 100644 index 000000000..6aa94f5c0 --- /dev/null +++ b/src/node/src/audio/pan/pan.cpp @@ -0,0 +1,132 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "pan.h" + +#include + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string PanNode::k_samples_input = "samples_in"; +const std::string PanNode::k_panning_input = "panning_in"; + +#define super Node + +PanNode::PanNode() +{ + add_input(k_samples_input, NodeValue::k_samples, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_panning_input, NodeValue::k_float, 0.0); + set_input_property(k_panning_input, "min", -1.0); + set_input_property(k_panning_input, "max", 1.0); + set_input_property(k_panning_input, "view", + slider::k_percentage); + + set_flag(k_audio_effect); + set_effect_input(k_samples_input); +} + +std::string PanNode::name() const +{ + return "Pan"; +} + +std::string PanNode::id() const +{ + return "org.olivevideoeditor.Olive.pan"; +} + +std::vector PanNode::category() const +{ + return { k_category_filter }; +} + +std::string PanNode::description() const +{ + return "Adjust the stereo panning of an audio source."; +} + +void PanNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Create a sample job + SampleBuffer samples = value.at(k_samples_input).to_samples(); + if (samples.is_allocated()) { + // This node is only compatible with stereo audio + if (samples.audio_params().channel_count() == 2) { + // If the input is static, we can just do it now which will be faster + if (is_input_static(k_panning_input)) { + float pan_volume = value.at(k_panning_input).to_double(); + if (pan_volume != 0.0f) { + if (pan_volume > 0) { + samples.transform_volume_for_channel(0, + 1.0f - pan_volume); + } else { + samples.transform_volume_for_channel(1, + 1.0f + pan_volume); + } + } + + table->push(NodeValue(NodeValue::k_samples, samples, this)); + } else { + // Requires job + SampleJob job(globals.time(), k_samples_input, value); + job.insert(k_panning_input, value); + table->push(NodeValue::k_samples, Variant::from_value(job), + this); + } + } else { + // Pass right through + table->push(value.at(k_samples_input)); + } + } +} + +void PanNode::process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const +{ + float pan_val = values.at(k_panning_input).to_double(); + + for (int i = 0; i < input.audio_params().channel_count(); i++) { + output.data(i)[index] = input.data(i)[index]; + } + + if (pan_val > 0) { + output.data(0)[index] *= (1.0F - pan_val); + } else if (pan_val < 0) { + output.data(1)[index] *= (1.0F - std::abs(pan_val)); + } +} + +void PanNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_samples_input, "Samples"); + set_input_name(k_panning_input, "Pan"); +} + +} diff --git a/src/node/src/audio/pan/pan.h b/src/node/src/audio/pan/pan.h new file mode 100644 index 000000000..ea28fc51c --- /dev/null +++ b/src/node/src/audio/pan/pan.h @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PANNODE_H +#define OAK_PANNODE_H + +#include "node.h" + +namespace olive +{ + +class PanNode : public Node { +public: + PanNode(); + + NODE_DEFAULT_FUNCTIONS(PanNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const override; + + virtual void retranslate() override; + + static const std::string k_samples_input; + static const std::string k_panning_input; + +private: + NodeInput *samples_input_; + NodeInput *panning_input_; +}; + +} + +#endif // OAK_PANNODE_H diff --git a/src/node/src/audio/volume/CMakeLists.txt b/src/node/src/audio/volume/CMakeLists.txt new file mode 100644 index 000000000..02f8eb768 --- /dev/null +++ b/src/node/src/audio/volume/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/audio/volume/volume.h + node/audio/volume/volume.cpp + PARENT_SCOPE +) diff --git a/src/node/src/audio/volume/volume.cpp b/src/node/src/audio/volume/volume.cpp new file mode 100644 index 000000000..c60d2a3c1 --- /dev/null +++ b/src/node/src/audio/volume/volume.cpp @@ -0,0 +1,114 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "volume.h" + +#include +#include + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string VolumeNode::k_samples_input = "samples_in"; +const std::string VolumeNode::k_volume_input = "volume_in"; + +#define super MathNodeBase + +VolumeNode::VolumeNode() +{ + add_input(k_samples_input, NodeValue::k_samples, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_volume_input, NodeValue::k_float, 1.0); + set_input_property(k_volume_input, "min", 0.0); + set_input_property(k_volume_input, "view", + slider::k_decibel); + + set_flag(k_audio_effect); + set_effect_input(k_samples_input); +} + +std::string VolumeNode::name() const +{ + return "Volume"; +} + +std::string VolumeNode::id() const +{ + return "org.olivevideoeditor.Olive.volume"; +} + +std::vector VolumeNode::category() const +{ + return { k_category_filter }; +} + +std::string VolumeNode::description() const +{ + return "Adjusts the volume of an audio source."; +} + +void VolumeNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Create a sample job + SampleBuffer buffer = value.at(k_samples_input).to_samples(); + + if (buffer.is_allocated()) { + // If the input is static, we can just do it now which will be faster + if (is_input_static(k_volume_input)) { + auto volume = value.at(k_volume_input).to_double(); + + // Same semantics as !qFuzzyCompare(volume, 1.0) (double overload) + if (std::abs(volume - 1.0) * 1000000000000.0 > + std::min(std::abs(volume), 1.0)) { + buffer.transform_volume(volume); + } + + table->push(NodeValue::k_samples, Variant::from_value(buffer), this); + } else { + // Requires job + SampleJob job(globals.time(), k_samples_input, value); + job.insert(k_volume_input, value); + table->push(NodeValue::k_samples, Variant::from_value(job), this); + } + } +} + +void VolumeNode::process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const +{ + return process_samples_internal(values, k_op_multiply, k_samples_input, + k_volume_input, input, output, index); +} + +void VolumeNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_samples_input, "Samples"); + set_input_name(k_volume_input, "Volume"); +} + +} diff --git a/src/node/src/audio/volume/volume.h b/src/node/src/audio/volume/volume.h new file mode 100644 index 000000000..e0091379e --- /dev/null +++ b/src/node/src/audio/volume/volume.h @@ -0,0 +1,56 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_VOLUMENODE_H +#define OAK_VOLUMENODE_H + +#include "math/math/mathbase.h" + +namespace olive +{ + +class VolumeNode : public MathNodeBase { +public: + VolumeNode(); + + NODE_DEFAULT_FUNCTIONS(VolumeNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const override; + + virtual void retranslate() override; + + static const std::string k_samples_input; + static const std::string k_volume_input; +}; + +} + +#endif // OAK_VOLUMENODE_H diff --git a/src/node/src/block/CMakeLists.txt b/src/node/src/block/CMakeLists.txt new file mode 100644 index 000000000..14362faae --- /dev/null +++ b/src/node/src/block/CMakeLists.txt @@ -0,0 +1,27 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(clip) +add_subdirectory(gap) +add_subdirectory(subtitle) +add_subdirectory(transition) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/block.h + node/block/block.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/block.cpp b/src/node/src/block/block.cpp new file mode 100644 index 000000000..efe0253b2 --- /dev/null +++ b/src/node/src/block/block.cpp @@ -0,0 +1,144 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "block.h" + +#include + +#include "inputdragger.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Node + +const std::string Block::k_length_input = "length_in"; + +Block::Block() + : previous_(nullptr) + , next_(nullptr) + , track_(nullptr) +{ + add_input(k_length_input, NodeValue::k_rational, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable | + k_input_flag_hidden)); + set_input_property(k_length_input, "min", + Variant::from_value(Rational(0, 1))); + set_input_property(k_length_input, "view", + slider::k_time); + set_input_property(k_length_input, "viewlock", true); + + set_input_flag(k_enabled_input, k_input_flag_not_connectable); + set_input_flag(k_enabled_input, k_input_flag_not_keyframable); + + set_flag(k_dont_show_in_param_view); +} + +std::vector Block::category() const +{ + return { k_category_timeline }; +} + +Rational Block::length() const +{ + return get_standard_value(k_length_input).value(); +} + +void Block::set_length_and_media_out(const Rational &length) +{ + if (length == this->length()) { + return; + } + + set_length_internal(length); +} + +void Block::set_length_and_media_in(const Rational &length) +{ + if (length == this->length()) { + return; + } + + // Set the length without setting media out + set_length_internal(length); +} + +bool Block::is_enabled() const +{ + return get_standard_value(k_enabled_input).to_bool(); +} + +void Block::set_enabled(bool e) +{ + set_standard_value(k_enabled_input, e); +} + +void Block::InputValueChangedEvent(const std::string &input, int element) +{ + super::InputValueChangedEvent(input, element); +} + +void Block::set_length_internal(const Rational &length) +{ + set_standard_value(k_length_input, Variant::from_value(length)); +} + +void Block::retranslate() +{ + super::retranslate(); + + set_input_name(k_length_input, "Length"); + set_input_name(k_enabled_input, "Enabled"); +} + +void Block::invalidate_cache(const TimeRange &range, const std::string &from, + int element, InvalidateCacheOptions options) +{ + TimeRange r; + + if (from == k_length_input) { + // We must intercept the signal here + r = TimeRange(std::min(length(), last_length_), RATIONAL_MAX); + + if (!NodeInputDragger::is_input_being_dragged()) { + last_length_ = length(); + } + + options["lengthevent"] = true; + } else { + r = range; + } + + super::invalidate_cache(r, from, element, options); +} + +void Block::set_previous_next(Block *previous, Block *next) +{ + if (previous) { + previous->set_next(next); + } + if (next) { + next->set_previous(previous); + } +} + +} diff --git a/src/node/src/block/block.h b/src/node/src/block/block.h new file mode 100644 index 000000000..f4197c451 --- /dev/null +++ b/src/node/src/block/block.h @@ -0,0 +1,133 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_BLOCK_H +#define OAK_BLOCK_H + +#include "node.h" +#include "timeline/timelinecommon.h" + +namespace olive +{ + +class TransitionBlock; + +/** + * @brief A Node that represents a block of time, also displayable on a Timeline + */ +class Block : public Node { +public: + Block(); + + virtual std::vector category() const override; + + const Rational &in() const + { + return in_point_; + } + + const Rational &out() const + { + return out_point_; + } + + void set_in(const Rational &in) + { + in_point_ = in; + } + + void set_out(const Rational &out) + { + out_point_ = out; + } + + Rational length() const; + virtual void set_length_and_media_out(const Rational &length); + virtual void set_length_and_media_in(const Rational &length); + + TimeRange range() const + { + return TimeRange(in(), out()); + } + + Block *previous() const + { + return previous_; + } + + Block *next() const + { + return next_; + } + + void set_previous(Block *previous) + { + previous_ = previous; + } + + void set_next(Block *next) + { + next_ = next; + } + + Track *track() const + { + return track_; + } + + void set_track(Track *track) + { + track_ = track; + } + + bool is_enabled() const; + void set_enabled(bool e); + + virtual void retranslate() override; + + virtual void invalidate_cache( + const TimeRange &range, const std::string &from, int element = -1, + InvalidateCacheOptions options = InvalidateCacheOptions()) override; + + static const std::string k_length_input; + + static void set_previous_next(Block *previous, Block *next); + +protected: + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + + Block *previous_; + Block *next_; + +private: + void set_length_internal(const Rational &length); + + Rational in_point_; + Rational out_point_; + Track *track_; + + Rational last_length_; +}; + +} + +#endif // OAK_BLOCK_H diff --git a/src/node/src/block/clip/CMakeLists.txt b/src/node/src/block/clip/CMakeLists.txt new file mode 100644 index 000000000..820529a96 --- /dev/null +++ b/src/node/src/block/clip/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/clip/clip.h + node/block/clip/clip.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/clip/clip.cpp b/src/node/src/block/clip/clip.cpp new file mode 100644 index 000000000..b80bdb6b1 --- /dev/null +++ b/src/node/src/block/clip/clip.cpp @@ -0,0 +1,591 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "clip.h" + +#include +#include + +#include "config/config.h" +#include "block/transition/transition.h" +#include "output/track/track.h" +#include "output/viewer/viewer.h" +#include "project/sequence/sequence.h" +#include "sliderdisplaytype.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Block + +const std::string ClipBlock::k_buffer_in = "buffer_in"; +const std::string ClipBlock::k_media_in_input = "media_in_in"; +const std::string ClipBlock::k_speed_input = "speed_in"; +const std::string ClipBlock::k_reverse_input = "reverse_in"; +const std::string ClipBlock::k_maintain_audio_pitch_input = + "maintain_audio_pitch_in"; +const std::string ClipBlock::k_auto_cache_input = "autocache_in"; +const std::string ClipBlock::k_loop_mode_input = "loop_in"; + +ClipBlock::ClipBlock() + : in_transition_(nullptr) + , out_transition_(nullptr) + , connected_viewer_(nullptr) +{ + add_input(k_media_in_input, NodeValue::k_rational, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + set_input_property(k_media_in_input, "view", + slider::k_time); + set_input_property(k_media_in_input, "viewlock", true); + + add_input(k_speed_input, NodeValue::k_float, 1.0, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + set_input_property(k_speed_input, "view", + slider::k_percentage); + set_input_property(k_speed_input, "min", 0.0); + + add_input(k_reverse_input, NodeValue::k_boolean, false, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_maintain_audio_pitch_input, NodeValue::k_boolean, false, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_auto_cache_input, NodeValue::k_boolean, false, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + prepend_input(k_buffer_in, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); + //SetValueHintForInput(kBufferIn, ValueHint(NodeValue::kBuffer)); + + set_effect_input(k_buffer_in); + + add_input(k_loop_mode_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); +} + +std::string ClipBlock::name() const +{ + if (connected_viewer_ && !connected_viewer_->get_label().empty()) { + return connected_viewer_->get_label(); + } else if (track()) { + if (track()->type() == Track::k_video) { + return "Video Clip"; + } else if (track()->type() == Track::k_audio) { + return "Audio Clip"; + } + } + + return "Clip"; +} + +std::string ClipBlock::id() const +{ + return "org.olivevideoeditor.Olive.clip"; +} + +std::string ClipBlock::description() const +{ + return "A time-based node that represents a media source."; +} + +void ClipBlock::set_length_and_media_out(const Rational &length) +{ + if (length == this->length()) { + return; + } + + if (reverse()) { + // Calculate media_in adjustment + Rational proposed_media_in = sequence_to_media_time( + this->length() - length, k_stm_ignore_reverse | k_stm_ignore_loop); + set_media_in(proposed_media_in); + } + + super::set_length_and_media_out(length); +} + +void ClipBlock::set_length_and_media_in(const Rational &length) +{ + if (length == this->length()) { + return; + } + + Rational old_length = this->length(); + + super::set_length_and_media_in(length); + + if (!reverse()) { + // Calculate media_in adjustment + set_media_in(sequence_to_media_time(old_length - length, k_stm_ignore_loop)); + } +} + +Rational ClipBlock::media_in() const +{ + return get_standard_value(k_media_in_input).value(); +} + +Node::ValueHint ClipBlock::get_value_hint_for_input(const std::string &input, + int element) const +{ + if (input == k_buffer_in) { + // The buffer input takes whatever the connected node provides, so it + // is declared as kNone and carries no stored hint. When the connected + // node pushes more than one value type (a footage pushes both a + // kTexture job and a kSamples job), a typeless lookup falls back to + // the last value in the table, which may feed audio samples into a + // video clip and produce a black frame. Prefer the value type that + // matches this clip's track. + switch (get_track_type()) { + case Track::k_video: + return ValueHint(std::vector{ NodeValue::k_texture }); + case Track::k_audio: + return ValueHint(std::vector{ NodeValue::k_samples }); + default: + break; + } + } + + return super::get_value_hint_for_input(input, element); +} + +void ClipBlock::set_media_in(const Rational &media_in) +{ + set_standard_value(k_media_in_input, Variant::from_value(media_in)); + + request_invalidated_from_connected(); +} + +void ClipBlock::set_autocache(bool e) +{ + set_standard_value(k_auto_cache_input, e); +} + +void ClipBlock::discard_cache() +{ + if (Node *connected = get_connected_output(k_buffer_in)) { + Track::Type type = get_track_type(); + if (type == Track::k_video) { + connected->video_frame_cache()->invalidate( + TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } else if (type == Track::k_audio) { + connected->audio_playback_cache()->invalidate( + TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } + } +} + +Rational ClipBlock::sequence_to_media_time(const Rational &sequence_time, + uint64_t flags) const +{ + // These constants are not considered "values" per se, so we don't modify them + if (sequence_time == RATIONAL_MIN || sequence_time == RATIONAL_MAX) { + return sequence_time; + } + + Rational media_time = sequence_time; + + if (reverse() && !(flags & k_stm_ignore_reverse)) { + media_time = length() - media_time; + } + + if (!(flags & k_stm_ignore_speed)) { + double speed_value = speed(); + if (speed_value == 0.0) { + // Effectively holds the frame at the in point + media_time = 0; + } else if (!(std::abs(speed_value - 1.0) * 1000000000000.0 <= + std::min(std::abs(speed_value), std::abs(1.0)))) { + // Multiply time + media_time = + Rational::from_double(media_time.to_double() * speed_value); + } + } + + media_time += media_in(); + + /*if (!(flags & kSTMIgnoreLoop) + && this->loop_mode() != kLoopModeOff + && connected_viewer_ + && !connected_viewer_->GetLength().isNull() + && (media_time < 0 || media_time >= connected_viewer_->GetLength())) { + if (loop_mode() == kLoopModeLoop) { + while (media_time < 0) { + media_time += connected_viewer_->GetLength(); + } + while (media_time >= connected_viewer_->GetLength()) { + media_time -= connected_viewer_->GetLength(); + } + } else if (loop_mode() == kLoopModeClamp) { + media_time = std::clamp(media_time, Rational(0), connected_viewer_->GetLength()-connected_viewer_->GetVideoParams().frame_rate_as_time_base()); + } + }*/ + + return media_time; +} + +Rational ClipBlock::media_to_sequence_time(const Rational &media_time) const +{ + // These constants are not considered "values" per se, so we don't modify them + if (media_time == RATIONAL_MIN || media_time == RATIONAL_MAX) { + return media_time; + } + + Rational sequence_time = media_time - media_in(); + + double speed_value = speed(); + if (speed_value == 0.0) { + // Speed zero holds the frame at the in point, so map to that frame + sequence_time = media_in(); + } else if (!(std::abs(speed_value - 1.0) * 1000000000000.0 <= + std::min(std::abs(speed_value), std::abs(1.0)))) { + // Divide time + sequence_time = + Rational::from_double(sequence_time.to_double() / speed_value); + } + + if (reverse()) { + sequence_time = length() - sequence_time; + } + + return sequence_time; +} + +void ClipBlock::request_range_from_connected(const TimeRange &range) +{ + Track::Type type = get_track_type(); + + if (type == Track::k_video || type == Track::k_audio) { + if (Node *connected = get_connected_output(k_buffer_in)) { + TimeRange max_range = media_range(); + if (type == Track::k_video) { + // Handle thumbnails + request_range_for_cache(connected->thumbnail_cache(), max_range, + range, true, false); + { + TimeRange thumb_range = range.intersected(max_range); + if (get_adjusted_thumbnail_range(&thumb_range)) { + connected->thumbnail_cache()->request( + this->track()->sequence(), thumb_range); + } + } + + // Handle video cache + request_range_for_cache(connected->video_frame_cache(), max_range, + range, true, is_autocaching()); + } else if (type == Track::k_audio) { + // Handle waveforms + request_range_for_cache( + connected->waveform_cache(), max_range, range, true, + (OAK_CONFIG("TimelineWaveformMode").to_int() == + Timeline::k_waveforms_enabled)); + + // Handle audio cache + request_range_for_cache(connected->audio_playback_cache(), + max_range, range, true, is_autocaching()); + } + } + } +} + +void ClipBlock::request_invalidated_from_connected(bool force_all, + const TimeRange &intersect) +{ + Track::Type type = get_track_type(); + + if (type == Track::k_video || type == Track::k_audio) { + if (Node *connected = get_connected_output(k_buffer_in)) { + TimeRange max_range = media_range(); + + if (!intersect.length().isNull()) { + max_range = max_range.intersected(intersect); + } + + if (type == Track::k_video) { + // Handle thumbnails + TimeRange thumb_range = max_range; + if (get_adjusted_thumbnail_range(&thumb_range)) { + request_invalidated_for_cache(connected->thumbnail_cache(), + thumb_range); + } + + // Handle video cache + if (is_autocaching() || force_all) { + request_invalidated_for_cache(connected->video_frame_cache(), + max_range); + } + } else if (type == Track::k_audio) { + // Handle waveforms + if (OAK_CONFIG("TimelineWaveformMode").to_int() == + Timeline::k_waveforms_enabled) { + request_invalidated_for_cache(connected->waveform_cache(), + max_range); + } + + // Handle audio cache + if (is_autocaching() || force_all) { + request_invalidated_for_cache( + connected->audio_playback_cache(), max_range); + } + } + } + } +} + +void ClipBlock::request_range_for_cache(PlaybackCache *cache, + const TimeRange &max_range, + const TimeRange &range, bool invalidate, + bool request) +{ + TimeRange r = range.intersected(max_range); + + if (invalidate) { + cache->invalidate(r); + } + + if (request) { + cache->request(this->track()->sequence(), r); + } +} + +void ClipBlock::request_invalidated_for_cache(PlaybackCache *cache, + const TimeRange &max_range) +{ + core::TimeRangeList invalid = cache->get_invalidated_ranges(max_range); + + for (const PlaybackCache::Passthrough &p : cache->get_passthroughs()) { + invalid.remove(p); + } + + for (const TimeRange &r : invalid) { + request_range_for_cache(cache, max_range, r, false, true); + } +} + +bool ClipBlock::get_adjusted_thumbnail_range(TimeRange *r) const +{ + switch (static_cast( + OAK_CONFIG("TimelineThumbnailMode").to_int())) { + case Timeline::k_thumbnail_off: + // Don't cache any range + return false; + case Timeline::k_thumbnail_in_out: { + // Only cache in point + Rational in = this->media_range().in(); + if (r->contains(in)) { + // Cache only the in point + *r = TimeRange(in, in + thumbnail_cache()->get_timebase()); + return true; + } else { + // Cache nothing + return false; + } + } + case Timeline::k_thumbnail_on: + // Cache entire range + return true; + } + + // Fallback + return true; +} + +void ClipBlock::invalidate_cache(const TimeRange &range, const std::string &from, + int element, InvalidateCacheOptions options) +{ + (void) element; + + // If signal is from texture input, transform all times from media time to sequence time + if (from == k_buffer_in) { + // Render caches where necessary + if (are_caches_enabled()) { + request_range_from_connected(range); + } + + // Adjust range from media time to sequence time + TimeRange adj; + double speed_value = speed(); + + if (speed_value == 0.0) { + // Handle 0 speed by invalidating the whole clip + adj = TimeRange(RATIONAL_MIN, RATIONAL_MAX); + } else { + adj = TimeRange(media_to_sequence_time(range.in()), + media_to_sequence_time(range.out())); + } + + // Find connected viewer node + auto viewers = find_input_nodes_connected_to_input( + NodeInput(this, k_buffer_in), 1); + ViewerOutput *new_connected_viewer = + viewers.empty() ? nullptr : viewers.front(); + + if (new_connected_viewer != connected_viewer_) { + connected_viewer_ = new_connected_viewer; + } + + super::invalidate_cache(adj, from, element, options); + } else { + // Otherwise, pass signal along normally + super::invalidate_cache(range, from, element, options); + } +} + +void ClipBlock::LinkChangeEvent() +{ + block_links_.clear(); + + for (Node *n : links()) { + ClipBlock *b = dynamic_cast(n); + + if (b) { + block_links_.push_back(b); + } + } +} + +void ClipBlock::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + super::InputConnectedEvent(input, element, output); +} + +void ClipBlock::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + super::InputDisconnectedEvent(input, element, output); +} + +void ClipBlock::InputValueChangedEvent(const std::string &input, int element) +{ + super::InputValueChangedEvent(input, element); + + if (input == k_auto_cache_input) { + if (is_autocaching()) { + request_invalidated_from_connected(); + } + } +} + +TimeRange ClipBlock::input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const +{ + (void) element; + + if (input == k_buffer_in) { + return TimeRange(sequence_to_media_time(input_time.in()), + sequence_to_media_time(input_time.out())); + } + + return super::input_time_adjustment(input, element, input_time, clamp); +} + +TimeRange ClipBlock::output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const +{ + (void) element; + + if (input == k_buffer_in) { + return TimeRange(media_to_sequence_time(input_time.in()), + media_to_sequence_time(input_time.out())); + } + + return super::output_time_adjustment(input, element, input_time); +} + +void ClipBlock::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + (void) globals; + + // We discard most values here except for the buffer we received + NodeValue data = value.at(k_buffer_in); + + table->clear(); + if (data.type() != NodeValue::k_none) { + table->push(data); + } +} + +void ClipBlock::retranslate() +{ + super::retranslate(); + + set_input_name(k_buffer_in, "Buffer"); + set_input_name(k_media_in_input, "Media In"); + set_input_name(k_speed_input, "Speed"); + set_input_name(k_reverse_input, "Reverse"); + set_input_name(k_maintain_audio_pitch_input, "Maintain Audio Pitch"); + set_input_name(k_loop_mode_input, "Loop"); + set_combo_box_strings(k_loop_mode_input, { "None", "Loop", "Clamp" }); +} + +void ClipBlock::add_cache_passthrough_from(ClipBlock *other) +{ + if (auto tc = this->video_frame_cache()) { + if (auto oc = other->video_frame_cache()) { + tc->set_passthrough(oc); + } + } + + if (auto tc = this->audio_playback_cache()) { + if (auto oc = other->audio_playback_cache()) { + tc->set_passthrough(oc); + } + } + + if (auto tc = this->thumbnails()) { + if (auto oc = other->thumbnails()) { + tc->set_passthrough(oc); + } + } + + if (auto tc = this->waveform()) { + if (auto oc = other->waveform()) { + tc->set_passthrough(oc); + } + } +} + +void ClipBlock::ConnectedToPreviewEvent() +{ + request_invalidated_from_connected(); +} + +TimeRange ClipBlock::media_range() const +{ + return input_time_adjustment(k_buffer_in, -1, TimeRange(0, length()), false); +} + +MultiCamNode *ClipBlock::find_multicam() +{ + auto v = find_input_nodes_connected_to_input( + NodeInput(this, k_buffer_in), 1); + if (v.empty()) { + return nullptr; + } else { + return v.front(); + } +} + +} diff --git a/src/node/src/block/clip/clip.h b/src/node/src/block/clip/clip.h new file mode 100644 index 000000000..2f4978b35 --- /dev/null +++ b/src/node/src/block/clip/clip.h @@ -0,0 +1,279 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_CLIPBLOCK_H +#define OAK_CLIPBLOCK_H + +#include "audio/audiovisualwaveform.h" +#include "codec/decoder.h" +#include "block/block.h" +#include "input/multicam/multicamnode.h" +#include "output/track/track.h" + +namespace olive +{ + +class ViewerOutput; + +/** + * @brief Node that represents a block of Media + */ +class ClipBlock : public Block { +public: + ClipBlock(); + + NODE_DEFAULT_FUNCTIONS(ClipBlock) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::string description() const override; + + virtual void set_length_and_media_out(const Rational &length) override; + virtual void set_length_and_media_in(const Rational &length) override; + + Track::Type get_track_type() const + { + if (track()) { + return track()->type(); + } else { + return Track::k_none; + } + } + + virtual Node::ValueHint + get_value_hint_for_input(const std::string &input, int element = -1) const override; + + Rational media_in() const; + void set_media_in(const Rational &media_in); + + bool is_autocaching() const + { + return get_standard_value(k_auto_cache_input).to_bool(); + } + void set_autocache(bool e); + + void discard_cache(); + + virtual void invalidate_cache(const TimeRange &range, const std::string &from, + int element, + InvalidateCacheOptions options) override; + + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const override; + + virtual TimeRange + output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void retranslate() override; + + void + request_invalidated_from_connected(bool force_all = false, + const TimeRange &intersect = TimeRange()); + + double speed() const + { + return get_standard_value(k_speed_input).to_double(); + } + + bool reverse() const + { + return get_standard_value(k_reverse_input).to_bool(); + } + + void set_reverse(bool e) + { + set_standard_value(k_reverse_input, e); + } + + bool maintain_audio_pitch() const + { + return get_standard_value(k_maintain_audio_pitch_input).to_bool(); + } + + void set_maintain_audio_pitch(bool e) + { + set_standard_value(k_maintain_audio_pitch_input, e); + } + + TransitionBlock *in_transition() + { + return in_transition_; + } + + void set_in_transition(TransitionBlock *t) + { + in_transition_ = t; + } + + TransitionBlock *out_transition() + { + return out_transition_; + } + + void set_out_transition(TransitionBlock *t) + { + out_transition_ = t; + } + + const std::vector &block_links() const + { + return block_links_; + } + + FrameHashCache *connected_video_cache() const + { + if (Node *n = get_connected_output(k_buffer_in)) { + return n->video_frame_cache(); + } else { + return nullptr; + } + } + + AudioPlaybackCache *connected_audio_cache() const + { + if (Node *n = get_connected_output(k_buffer_in)) { + return n->audio_playback_cache(); + } else { + return nullptr; + } + } + + ThumbnailCache *thumbnails() + { + if (Node *n = get_connected_output(k_buffer_in)) { + return n->thumbnail_cache(); + } else { + return nullptr; + } + } + + AudioWaveformCache *waveform() + { + if (Node *n = get_connected_output(k_buffer_in)) { + return n->waveform_cache(); + } else { + return nullptr; + } + } + + void add_cache_passthrough_from(ClipBlock *other); + + ViewerOutput *connected_viewer() const + { + return connected_viewer_; + } + + virtual TimeRange get_video_cache_range() const override + { + return TimeRange(0, length()); + } + + virtual TimeRange get_audio_cache_range() const override + { + return TimeRange(0, length()); + } + + virtual void ConnectedToPreviewEvent() override; + + TimeRange media_range() const; + + /** + * @brief Get currently set loop mode + */ + LoopMode loop_mode() const + { + return static_cast(get_standard_value(k_loop_mode_input).to_int()); + } + + void set_loop_mode(LoopMode l) + { + set_standard_value(k_loop_mode_input, int(l)); + } + + MultiCamNode *find_multicam(); + + static const std::string k_buffer_in; + static const std::string k_media_in_input; + static const std::string k_speed_input; + static const std::string k_reverse_input; + static const std::string k_maintain_audio_pitch_input; + static const std::string k_loop_mode_input; + + static const std::string k_auto_cache_input; + +protected: + virtual void LinkChangeEvent() override; + + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + +private: + enum SequenceToMediaTimeFlag { + k_stm_none = 0x0, + k_stm_ignore_reverse = 0x1, + k_stm_ignore_speed = 0x2, + k_stm_ignore_loop = 0x4 + }; + + Rational sequence_to_media_time(const Rational &sequence_time, + uint64_t flags = k_stm_none) const; + + Rational media_to_sequence_time(const Rational &media_time) const; + + void request_range_from_connected(const TimeRange &range); + + void request_range_for_cache(PlaybackCache *cache, const TimeRange &max_range, + const TimeRange &range, bool invalidate, + bool request); + void request_invalidated_for_cache(PlaybackCache *cache, + const TimeRange &max_range); + + bool get_adjusted_thumbnail_range(TimeRange *r) const; + + std::vector block_links_; + + TransitionBlock *in_transition_; + TransitionBlock *out_transition_; + + // NOTE: in the Qt version this was cleared via the viewer's destroyed() + // signal (see invalidate_cache); with signals removed from oaknode, the + // facade layer must ensure the viewer outlives this clip or clears this + // pointer. + ViewerOutput *connected_viewer_; + +private: + Rational last_media_in_; +}; + +} + +#endif // TIMELINEBLOCK_H diff --git a/src/node/src/block/gap/CMakeLists.txt b/src/node/src/block/gap/CMakeLists.txt new file mode 100644 index 000000000..f24a2fb89 --- /dev/null +++ b/src/node/src/block/gap/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/gap/gap.h + node/block/gap/gap.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/gap/gap.cpp b/src/node/src/block/gap/gap.cpp new file mode 100644 index 000000000..0fad54aef --- /dev/null +++ b/src/node/src/block/gap/gap.cpp @@ -0,0 +1,46 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "gap.h" + +namespace olive +{ + +GapBlock::GapBlock() +{ +} + +std::string GapBlock::name() const +{ + return "Gap"; +} + +std::string GapBlock::id() const +{ + return "org.olivevideoeditor.Olive.gap"; +} + +std::string GapBlock::description() const +{ + return "A time-based node that represents an empty space."; +} + +} diff --git a/src/node/src/block/gap/gap.h b/src/node/src/block/gap/gap.h new file mode 100644 index 000000000..824f6ef6b --- /dev/null +++ b/src/node/src/block/gap/gap.h @@ -0,0 +1,46 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_GAPBLOCK_H +#define OAK_GAPBLOCK_H + +#include "block/block.h" + +namespace olive +{ + +/** + * @brief Node that represents nothing in its respective track for a certain period of time + */ +class GapBlock : public Block { +public: + GapBlock(); + + NODE_DEFAULT_FUNCTIONS(GapBlock) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::string description() const override; +}; + +} + +#endif // TIMELINEBLOCK_H diff --git a/src/node/src/block/subtitle/CMakeLists.txt b/src/node/src/block/subtitle/CMakeLists.txt new file mode 100644 index 000000000..5d29ac15a --- /dev/null +++ b/src/node/src/block/subtitle/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/subtitle/subtitle.cpp + node/block/subtitle/subtitle.h + PARENT_SCOPE +) diff --git a/src/node/src/block/subtitle/subtitle.cpp b/src/node/src/block/subtitle/subtitle.cpp new file mode 100644 index 000000000..07e38cab9 --- /dev/null +++ b/src/node/src/block/subtitle/subtitle.cpp @@ -0,0 +1,73 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "subtitle.h" + +namespace olive +{ + +#define super ClipBlock + +const std::string SubtitleBlock::k_text_in = "text_in"; + +SubtitleBlock::SubtitleBlock() +{ + add_input(k_text_in, NodeValue::k_text, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + set_input_flag(k_buffer_in, k_input_flag_hidden); + set_input_flag(k_length_input, k_input_flag_hidden); + set_input_flag(k_media_in_input, k_input_flag_hidden); + set_input_flag(k_speed_input, k_input_flag_hidden); + set_input_flag(k_reverse_input, k_input_flag_hidden); + set_input_flag(k_maintain_audio_pitch_input, k_input_flag_hidden); + + // Undo block flag that hides in param view + set_flag(k_dont_show_in_param_view, false); +} + +std::string SubtitleBlock::name() const +{ + if (get_text().empty()) { + return "Subtitle"; + } else { + return get_text(); + } +} + +std::string SubtitleBlock::id() const +{ + return "org.olivevideoeditor.Olive.subtitle"; +} + +std::string SubtitleBlock::description() const +{ + return "A time-based node representing a single subtitle element for a certain period of time."; +} + +void SubtitleBlock::retranslate() +{ + super::retranslate(); + + set_input_name(k_text_in, "Text"); +} + +} diff --git a/src/node/src/block/subtitle/subtitle.h b/src/node/src/block/subtitle/subtitle.h new file mode 100644 index 000000000..876529a09 --- /dev/null +++ b/src/node/src/block/subtitle/subtitle.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SUBTITLEBLOCK_H +#define OAK_SUBTITLEBLOCK_H + +#include "block/clip/clip.h" + +namespace olive +{ + +class SubtitleBlock : public ClipBlock { +public: + SubtitleBlock(); + + NODE_DEFAULT_FUNCTIONS(SubtitleBlock) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + static const std::string k_text_in; + + std::string get_text() const + { + return get_standard_value(k_text_in).to_string(); + } + + void set_text(const std::string &text) + { + set_standard_value(k_text_in, text); + } +}; + +} + +#endif // OAK_SUBTITLEBLOCK_H diff --git a/src/node/src/block/transition/CMakeLists.txt b/src/node/src/block/transition/CMakeLists.txt new file mode 100644 index 000000000..a1f8bfb9b --- /dev/null +++ b/src/node/src/block/transition/CMakeLists.txt @@ -0,0 +1,25 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(crossdissolve) +add_subdirectory(diptocolor) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/transition.h + node/block/transition/transition.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/transition/crossdissolve/CMakeLists.txt b/src/node/src/block/transition/crossdissolve/CMakeLists.txt new file mode 100644 index 000000000..bb91a14b9 --- /dev/null +++ b/src/node/src/block/transition/crossdissolve/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/crossdissolve/crossdissolvetransition.h + node/block/transition/crossdissolve/crossdissolvetransition.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/transition/crossdissolve/crossdissolvetransition.cpp b/src/node/src/block/transition/crossdissolve/crossdissolvetransition.cpp new file mode 100644 index 000000000..23e70d262 --- /dev/null +++ b/src/node/src/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -0,0 +1,97 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "crossdissolvetransition.h" + +#include "filefunctions.h" + +namespace olive +{ + +CrossDissolveTransition::CrossDissolveTransition() +{ +} + +std::string CrossDissolveTransition::name() const +{ + return "Cross Dissolve"; +} + +std::string CrossDissolveTransition::id() const +{ + return "org.olivevideoeditor.Olive.crossdissolve"; +} + +std::vector CrossDissolveTransition::category() const +{ + return { k_category_transition }; +} + +std::string CrossDissolveTransition::description() const +{ + return "Smoothly transition between two clips."; +} + +ShaderCode +CrossDissolveTransition::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/crossdissolve.frag"), + std::string()); +} + +void CrossDissolveTransition::SampleJobEvent(const SampleBuffer &from_samples, + const SampleBuffer &to_samples, + SampleBuffer &out_samples, + double time_in) const +{ + for (size_t i = 0; i < out_samples.sample_count(); i++) { + double this_sample_time = + out_samples.audio_params().samples_to_time(i).to_double() + time_in; + double progress = get_total_progress(this_sample_time); + + for (int j = 0; j < out_samples.audio_params().channel_count(); j++) { + out_samples.data(j)[i] = 0; + + if (from_samples.is_allocated()) { + if (i < from_samples.sample_count()) { + out_samples.data(j)[i] += from_samples.data(j)[i] * + transform_curve(1.0 - progress); + } + } + + if (to_samples.is_allocated()) { + // Offset input samples from the end + size_t remain = + (out_samples.sample_count() - to_samples.sample_count()); + if (i >= remain) { + int64_t in_index = i - remain; + out_samples.data(j)[i] += + to_samples.data(j)[in_index] * transform_curve(progress); + } + } + } + } +} + +} diff --git a/src/node/src/block/transition/crossdissolve/crossdissolvetransition.h b/src/node/src/block/transition/crossdissolve/crossdissolvetransition.h new file mode 100644 index 000000000..057dc2ad9 --- /dev/null +++ b/src/node/src/block/transition/crossdissolve/crossdissolvetransition.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_CROSSDISSOLVETRANSITION_H +#define OAK_CROSSDISSOLVETRANSITION_H + +#include "block/transition/transition.h" + +namespace olive +{ + +class CrossDissolveTransition : public TransitionBlock { +public: + CrossDissolveTransition(); + + NODE_DEFAULT_FUNCTIONS(CrossDissolveTransition) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + //virtual void Retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + +protected: + virtual void SampleJobEvent(const SampleBuffer &from_samples, + const SampleBuffer &to_samples, + SampleBuffer &out_samples, + double time_in) const override; +}; + +} + +#endif // OAK_CROSSDISSOLVETRANSITION_H diff --git a/src/node/src/block/transition/diptocolor/CMakeLists.txt b/src/node/src/block/transition/diptocolor/CMakeLists.txt new file mode 100644 index 000000000..d66e47ad1 --- /dev/null +++ b/src/node/src/block/transition/diptocolor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/diptocolor/diptocolortransition.h + node/block/transition/diptocolor/diptocolortransition.cpp + PARENT_SCOPE +) diff --git a/src/node/src/block/transition/diptocolor/diptocolortransition.cpp b/src/node/src/block/transition/diptocolor/diptocolortransition.cpp new file mode 100644 index 000000000..d3633d101 --- /dev/null +++ b/src/node/src/block/transition/diptocolor/diptocolortransition.cpp @@ -0,0 +1,82 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "diptocolortransition.h" + +#include "filefunctions.h" + +namespace olive +{ + +const std::string DipToColorTransition::k_color_input = "color_in"; + +#define super TransitionBlock + +DipToColorTransition::DipToColorTransition() +{ + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(0, 0, 0))); +} + +std::string DipToColorTransition::name() const +{ + return "Dip To Color"; +} + +std::string DipToColorTransition::id() const +{ + return "org.olivevideoeditor.Olive.diptocolor"; +} + +std::vector DipToColorTransition::category() const +{ + return { k_category_transition }; +} + +std::string DipToColorTransition::description() const +{ + return "Transition between clips by dipping to a color."; +} + +ShaderCode +DipToColorTransition::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/diptoblack.frag"), + std::string()); +} + +void DipToColorTransition::retranslate() +{ + super::retranslate(); + + set_input_name(k_color_input, "Color"); +} + +void DipToColorTransition::ShaderJobEvent(const NodeValueRow &value, + ShaderJob *job) const +{ + job->insert(k_color_input, value); +} + +} diff --git a/src/node/src/block/transition/diptocolor/diptocolortransition.h b/src/node/src/block/transition/diptocolor/diptocolortransition.h new file mode 100644 index 000000000..e6fd1dd67 --- /dev/null +++ b/src/node/src/block/transition/diptocolor/diptocolortransition.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DIPTOCOLORTRANSITION_H +#define OAK_DIPTOCOLORTRANSITION_H + +#include "block/transition/transition.h" + +namespace olive +{ + +class DipToColorTransition : public TransitionBlock { +public: + DipToColorTransition(); + + NODE_DEFAULT_FUNCTIONS(DipToColorTransition) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + virtual void retranslate() override; + + static const std::string k_color_input; + +protected: + virtual void ShaderJobEvent(const NodeValueRow &value, + ShaderJob *job) const override; +}; + +} + +#endif // OAK_DIPTOCOLORTRANSITION_H diff --git a/src/node/src/block/transition/transition.cpp b/src/node/src/block/transition/transition.cpp new file mode 100644 index 000000000..ca8c0c8f0 --- /dev/null +++ b/src/node/src/block/transition/transition.cpp @@ -0,0 +1,343 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "transition.h" + +#include "block/clip/clip.h" +#include "output/track/track.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Block + +const std::string TransitionBlock::k_out_block_input = "out_block_in"; +const std::string TransitionBlock::k_in_block_input = "in_block_in"; +const std::string TransitionBlock::k_curve_input = "curve_in"; +const std::string TransitionBlock::k_center_input = "center_in"; + +TransitionBlock::TransitionBlock() + : connected_out_block_(nullptr) + , connected_in_block_(nullptr) +{ + add_input(k_out_block_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_in_block_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_curve_input, NodeValue::k_combo, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + + add_input(k_center_input, NodeValue::k_rational, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + set_input_property(k_center_input, "view", slider::k_time); + set_input_property(k_center_input, "viewlock", true); + + set_flag(k_dont_show_in_param_view, false); +} + +void TransitionBlock::retranslate() +{ + super::retranslate(); + + set_input_name(k_out_block_input, "From"); + set_input_name(k_in_block_input, "To"); + set_input_name(k_curve_input, "Curve"); + set_input_name(k_center_input, "Center Offset"); + + // These must correspond to the CurveType enum + set_combo_box_strings(k_curve_input, + { "Linear", "Exponential", "Logarithmic" }); +} + +Rational TransitionBlock::in_offset() const +{ + if (is_dual_transition()) { + return length() / 2 + offset_center(); + } else if (connected_in_block()) { + return length(); + } else { + return 0; + } +} + +Rational TransitionBlock::out_offset() const +{ + if (is_dual_transition()) { + return length() / 2 - offset_center(); + } else if (connected_out_block()) { + return length(); + } else { + return 0; + } +} + +Rational TransitionBlock::offset_center() const +{ + return get_standard_value(k_center_input).value(); +} + +void TransitionBlock::set_offset_center(const Rational &r) +{ + set_standard_value(k_center_input, Variant::from_value(r)); +} + +void TransitionBlock::set_offsets_and_length(const Rational &in_offset, + const Rational &out_offset) +{ + Rational len = in_offset + out_offset; + Rational center = len / 2 - in_offset; + + set_length_and_media_out(len); + set_offset_center(center); +} + +Block *TransitionBlock::connected_out_block() const +{ + return connected_out_block_; +} + +Block *TransitionBlock::connected_in_block() const +{ + return connected_in_block_; +} + +double TransitionBlock::get_total_progress(const double &time) const +{ + return get_internal_transition_time(time) / length().to_double(); +} + +double TransitionBlock::get_out_progress(const double &time) const +{ + if (out_offset() == 0) { + return 0; + } + + return std::clamp( + 1.0 - (get_internal_transition_time(time) / out_offset().to_double()), 0.0, + 1.0); +} + +double TransitionBlock::get_in_progress(const double &time) const +{ + if (in_offset() == 0) { + return 0; + } + + return std::clamp( + (get_internal_transition_time(time) - out_offset().to_double()) / + in_offset().to_double(), + 0.0, 1.0); +} + +double TransitionBlock::get_internal_transition_time(const double &time) const +{ + return time; +} + +void TransitionBlock::insert_transition_times(AcceleratedJob *job, + const double &time) const +{ + // Provides total transition progress from 0.0 (start) - 1.0 (end) + job->insert("ove_tprog_all", + NodeValue(NodeValue::k_float, get_total_progress(time), this)); + + // Provides progress of out section from 1.0 (start) - 0.0 (end) + job->insert("ove_tprog_out", + NodeValue(NodeValue::k_float, get_out_progress(time), this)); + + // Provides progress of in section from 0.0 (start) - 1.0 (end) + job->insert("ove_tprog_in", + NodeValue(NodeValue::k_float, get_in_progress(time), this)); +} + +void TransitionBlock::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + NodeValue out_buffer = value.at(k_out_block_input); + NodeValue in_buffer = value.at(k_in_block_input); + NodeValue::Type data_type = (out_buffer.type() != NodeValue::k_none) ? + out_buffer.type() : + in_buffer.type(); + + NodeValue::Type job_type = NodeValue::k_none; + Variant push_job; + + if (data_type == NodeValue::k_texture) { + // This must be a visual transition + ShaderJob job; + + if (out_buffer.type() != NodeValue::k_none) { + job.insert(k_out_block_input, out_buffer); + } else { + job.insert(k_out_block_input, NodeValue(NodeValue::k_texture, nullptr)); + } + + if (in_buffer.type() != NodeValue::k_none) { + job.insert(k_in_block_input, in_buffer); + } else { + job.insert(k_in_block_input, NodeValue(NodeValue::k_texture, nullptr)); + } + + job.insert(k_curve_input, value); + + double time = globals.time().in().to_double(); + insert_transition_times(&job, time); + + ShaderJobEvent(value, &job); + + job_type = NodeValue::k_texture; + push_job = Variant::from_value(Texture::job(globals.vparams(), job)); + } else if (data_type == NodeValue::k_samples) { + // This must be an audio transition + SampleBuffer from_samples = out_buffer.to_samples(); + SampleBuffer to_samples = in_buffer.to_samples(); + + if (from_samples.is_allocated() || to_samples.is_allocated()) { + double time_in = globals.time().in().to_double(); + double time_out = globals.time().out().to_double(); + + const AudioParams ¶ms = (from_samples.is_allocated()) ? + from_samples.audio_params() : + to_samples.audio_params(); + + SampleBuffer out_samples; + + if (params.is_valid()) { + int nb_samples = params.time_to_samples(time_out - time_in); + + out_samples = SampleBuffer(params, nb_samples); + SampleJobEvent(from_samples, to_samples, out_samples, time_in); + } + + job_type = NodeValue::k_samples; + push_job = Variant::from_value(out_samples); + } + } + + if (!push_job.is_null()) { + table->push(job_type, push_job, this); + } +} + +void TransitionBlock::invalidate_cache(const TimeRange &range, + const std::string &from, int element, + InvalidateCacheOptions options) +{ + TimeRange r = range; + + if (from == k_out_block_input || from == k_in_block_input) { + Block *n = dynamic_cast(get_connected_output(from)); + if (n) { + r = Track::transform_range_from_block(n, r); + } + } + + super::invalidate_cache(r, from, element, options); +} + +double TransitionBlock::transform_curve(double linear) const +{ + switch (static_cast(get_standard_value(k_curve_input).to_int())) { + case k_linear: + break; + case k_exponential: + linear *= linear; + break; + case k_logarithmic: + linear = std::sqrt(linear); + break; + } + + return linear; +} + +void TransitionBlock::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + (void) element; + + if (input == k_out_block_input) { + // If node is not a block, this will just be null + if ((connected_out_block_ = dynamic_cast(output))) { + connected_out_block_->set_out_transition(this); + } + } else if (input == k_in_block_input) { + // If node is not a block, this will just be null + if ((connected_in_block_ = dynamic_cast(output))) { + connected_in_block_->set_in_transition(this); + } + } +} + +void TransitionBlock::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + (void) element; + (void) output; + + if (input == k_out_block_input) { + if (connected_out_block_) { + connected_out_block_->set_out_transition(nullptr); + connected_out_block_ = nullptr; + } + } else if (input == k_in_block_input) { + if (connected_in_block_) { + connected_in_block_->set_in_transition(nullptr); + connected_in_block_ = nullptr; + } + } +} + +TimeRange TransitionBlock::input_time_adjustment(const std::string &input, + int element, + const TimeRange &input_time, + bool clamp) const +{ + if (input == k_in_block_input || input == k_out_block_input) { + Block *block = dynamic_cast(get_connected_output(input)); + if (block) { + // Retransform time as if it came from the track + return input_time + in() - block->in(); + } + } + + return super::input_time_adjustment(input, element, input_time, clamp); +} + +TimeRange +TransitionBlock::output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const +{ + if (input == k_in_block_input || input == k_out_block_input) { + Block *block = dynamic_cast(get_connected_output(input)); + if (block) { + return input_time + block->in() - in(); + } + } + + return super::output_time_adjustment(input, element, input_time); +} + +} diff --git a/src/node/src/block/transition/transition.h b/src/node/src/block/transition/transition.h new file mode 100644 index 000000000..026c16eea --- /dev/null +++ b/src/node/src/block/transition/transition.h @@ -0,0 +1,120 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TRANSITIONBLOCK_H +#define OAK_TRANSITIONBLOCK_H + +#include "block/block.h" + +namespace olive +{ + +class ClipBlock; + +class TransitionBlock : public Block { +public: + TransitionBlock(); + + virtual void retranslate() override; + + Rational in_offset() const; + Rational out_offset() const; + + /** + * @brief Return the "middle point" of the transition, relative to the transition + * + * Used to calculate in/out offsets. + * + * 0 means the center of the transition is right in the middle and the in and out offsets will + * be equal. + */ + Rational offset_center() const; + void set_offset_center(const Rational &r); + + void set_offsets_and_length(const Rational &in_offset, + const Rational &out_offset); + + bool is_dual_transition() const + { + return connected_out_block() && connected_in_block(); + } + + Block *connected_out_block() const; + Block *connected_in_block() const; + + double get_total_progress(const double &time) const; + double get_out_progress(const double &time) const; + double get_in_progress(const double &time) const; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void invalidate_cache( + const TimeRange &range, const std::string &from, int element = -1, + InvalidateCacheOptions options = InvalidateCacheOptions()) override; + + static const std::string k_out_block_input; + static const std::string k_in_block_input; + static const std::string k_curve_input; + static const std::string k_center_input; + +protected: + virtual void ShaderJobEvent(const NodeValueRow &value, ShaderJob *job) const + { + } + + virtual void SampleJobEvent(const SampleBuffer &from_samples, + const SampleBuffer &to_samples, + SampleBuffer &out_samples, double time_in) const + { + } + + double transform_curve(double linear) const; + + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const override; + + virtual TimeRange + output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const override; + +private: + enum CurveType { k_linear, k_exponential, k_logarithmic }; + + double get_internal_transition_time(const double &time) const; + + void insert_transition_times(AcceleratedJob *job, const double &time) const; + + ClipBlock *connected_out_block_; + + ClipBlock *connected_in_block_; +}; + +} + +#endif // OAK_TRANSITIONBLOCK_H diff --git a/src/node/src/color/CMakeLists.txt b/src/node/src/color/CMakeLists.txt new file mode 100644 index 000000000..1740dcc90 --- /dev/null +++ b/src/node/src/color/CMakeLists.txt @@ -0,0 +1,29 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(colormanager) +add_subdirectory(displaytransform) +add_subdirectory(ociobase) +add_subdirectory(ociogradingtransformlinear) +add_subdirectory(ociogradingtransformlog) +add_subdirectory(ociolut) +add_subdirectory(threewaycolor) +add_subdirectory(whitebalance) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/color/colormanager/CMakeLists.txt b/src/node/src/color/colormanager/CMakeLists.txt new file mode 100644 index 000000000..c379f8ea9 --- /dev/null +++ b/src/node/src/color/colormanager/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/colormanager/colormanager.cpp + node/color/colormanager/colormanager.h + PARENT_SCOPE +) diff --git a/src/node/src/color/colormanager/colormanager.cpp b/src/node/src/color/colormanager/colormanager.cpp new file mode 100644 index 000000000..0ffcf08ff --- /dev/null +++ b/src/node/src/color/colormanager/colormanager.cpp @@ -0,0 +1,342 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "colormanager.h" + +#include +#include +#include +#include + +#include "define.h" +#include "filefunctions.h" +#include "config/config.h" +#include "project.h" + +namespace olive +{ + +ocio::ConstConfigRcPtr ColorManager::default_config = nullptr; + +ColorManager::ColorManager(Project *project) + : project_(project) + , config_(nullptr) +{ +} + +void ColorManager::init() +{ + // Set config to our built-in default + config_ = get_default_config(); + set_default_input_color_space(config_->getCanonicalName(ocio::ROLE_DEFAULT)); + project()->set_color_reference_space(ocio::ROLE_SCENE_LINEAR); +} + +ocio::ConstConfigRcPtr ColorManager::get_config() const +{ + return config_; +} + +ocio::ConstConfigRcPtr +ColorManager::create_config_from_file(const std::string &filename) +{ + return ocio::Config::CreateFromFile(filename.c_str()); +} + +std::string ColorManager::get_config_filename() const +{ + return project()->get_color_config_filename(); +} + +ocio::ConstConfigRcPtr ColorManager::get_default_config() +{ + // Set up on first use: Project construction calls ColorManager::Init() + // unconditionally, so without this any Project created before + // SetUpDefaultConfig() crashed dereferencing a null config. + if (!default_config) { + set_up_default_config(); + } + + return default_config; +} + +void ColorManager::set_up_default_config() +{ + const char *ocio_env = std::getenv("OCIO"); + if (ocio_env != nullptr && ocio_env[0] != '\0') { + // Attempt to set config from "OCIO" environment variable + try { + default_config = ocio::Config::CreateFromEnv(); + + return; + } catch (ocio::Exception &e) { + fprintf(stderr, + "Failed to load config from OCIO environment variable config: %s\n", + e.what()); + } + } + + // Extract OCIO config - kind of hacky, but it'll work + // NOTE: was QStandardPaths::CacheLocation; oakcommon's configuration + // location is the closest Qt-free persistent directory available here. + std::string dir = FileFunctions::get_configuration_location() + "/ocioconf"; + + FileFunctions::copy_directory(":/ocioconf", dir, true); + + fprintf(stderr, "Extracting default OCIO config to %s\n", dir.c_str()); + + default_config = create_config_from_file(dir + "/config.ocio"); +} + +void ColorManager::set_config_filename(const std::string &filename) +{ + project()->set_color_config_filename(filename); +} + +StringList ColorManager::list_available_displays() +{ + StringList displays; + + int number_of_displays = config_->getNumDisplays(); + + for (int i = 0; i < number_of_displays; i++) { + displays.push_back(config_->getDisplay(i)); + } + + return displays; +} + +std::string ColorManager::get_default_display() +{ + return config_->getDefaultDisplay(); +} + +StringList ColorManager::list_available_views(std::string display) +{ + StringList views; + + int number_of_views = config_->getNumViews(display.c_str()); + + for (int i = 0; i < number_of_views; i++) { + views.push_back(config_->getView(display.c_str(), i)); + } + + return views; +} + +std::string ColorManager::get_default_view(const std::string &display) +{ + return config_->getDefaultView(display.c_str()); +} + +StringList ColorManager::list_available_looks() +{ + StringList looks; + + int number_of_looks = config_->getNumLooks(); + + for (int i = 0; i < number_of_looks; i++) { + looks.push_back(config_->getLookNameByIndex(i)); + } + + return looks; +} + +StringList ColorManager::list_available_colorspaces() const +{ + return list_available_colorspaces(config_); +} + +std::string ColorManager::get_default_input_color_space() const +{ + return project()->get_default_input_color_space(); +} + +void ColorManager::set_default_input_color_space(const std::string &s) +{ + project()->set_default_input_color_space(s); +} + +std::string ColorManager::get_colorspace_for_ffmpeg_tags(int primaries, + int trc) const +{ + // FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values mapped to + // candidate colorspace names, in order of preference + struct TagMapping { + int primaries; + int trc; + const char *candidates[3]; + }; + + static const TagMapping k_tag_mappings[] = { + { 1, 1, { "Rec.709 OETF", "Rec.709", "BT.709" } }, + { 1, 13, { "sRGB OETF", "sRGB", nullptr } }, + { 6, 6, { "Rec.601 OETF (NTSC)", "Rec.601 NTSC", nullptr } }, + { 5, 5, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } }, + { 5, 6, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } }, + { 9, 16, { "Rec.2020 PQ", "BT.2020 PQ", "ST 2084 PQ" } }, + { 9, 18, { "Rec.2020 HLG", "BT.2020 HLG", "HLG" } }, + { 9, 1, { "Rec.2020", "BT.2020", nullptr } }, + { 9, 14, { "Rec.2020", "BT.2020", nullptr } }, + { 9, 15, { "Rec.2020", "BT.2020", nullptr } }, + }; + + // 0 = unset, 2 = AVCOL_PRI/TRC_UNSPECIFIED + if (primaries == 0 || primaries == 2 || trc == 0 || trc == 2) { + return std::string(); + } + + const StringList available = list_available_colorspaces(); + + for (const TagMapping &mapping : k_tag_mappings) { + if (mapping.primaries == primaries && mapping.trc == trc) { + for (const char *candidate : mapping.candidates) { + if (candidate && + std::find(available.begin(), available.end(), + std::string(candidate)) != available.end()) { + return candidate; + } + } + // Known tag pair, but the config has no matching colorspace + return std::string(); + } + } + + return std::string(); +} + +std::string ColorManager::get_reference_color_space() const +{ + return project()->get_color_reference_space(); +} + +std::string ColorManager::get_compliant_color_space(const std::string &s) +{ + const StringList available = list_available_colorspaces(); + if (std::find(available.begin(), available.end(), s) != available.end()) { + return s; + } else { + return get_default_input_color_space(); + } +} + +ColorTransform +ColorManager::get_compliant_color_space(const ColorTransform &transform, + bool force_display) +{ + if (transform.is_display() || force_display) { + // Get display information + std::string display = transform.display(); + std::string view = transform.view(); + std::string look = transform.look(); + + const StringList displays = list_available_displays(); + + // Check if display still exists in config + if (std::find(displays.begin(), displays.end(), display) == + displays.end()) { + display = get_default_display(); + } + + const StringList views = list_available_views(display); + + // Check if view still exists in display + if (std::find(views.begin(), views.end(), view) == views.end()) { + view = get_default_view(display); + } + + const StringList looks = list_available_looks(); + + // Check if looks still exists + if (std::find(looks.begin(), looks.end(), look) == looks.end()) { + look.clear(); + } + + return ColorTransform(display, view, look); + + } else { + std::string output = transform.output(); + + const StringList colorspaces = list_available_colorspaces(); + + if (std::find(colorspaces.begin(), colorspaces.end(), output) == + colorspaces.end()) { + output = get_default_input_color_space(); + } + + return ColorTransform(output); + } +} + +StringList +ColorManager::list_available_colorspaces(ocio::ConstConfigRcPtr config) +{ + StringList spaces; + + if (config) { + int number_of_colorspaces = config->getNumColorSpaces(); + + for (int i = 0; i < number_of_colorspaces; i++) { + spaces.push_back(config->getColorSpaceNameByIndex(i)); + } + } + + return spaces; +} + +void ColorManager::get_default_luma_coefs(double *rgb) const +{ + config_->getDefaultLumaCoefs(rgb); +} + +Project *ColorManager::project() const +{ + return project_; +} + +void ColorManager::update_config_from_filename() +{ + try { + std::string config_filename = get_config_filename(); + std::string old_default_cs = get_default_input_color_space(); + + config_ = ocio::Config::CreateFromFile(config_filename.c_str()); + + // Set new default colorspace appropriately + std::string new_default = old_default_cs; + StringList available_cs = list_available_colorspaces(); + for (const std::string &c : available_cs) { + // NOTE: preserves the original Qt code's truthiness semantics + // (QString::compare(...) != 0, i.e. case-insensitively different) + if (strcasecmp(c.c_str(), old_default_cs.c_str())) { + new_default = c; + break; + } + } + set_default_input_color_space(new_default); + + // The former config_changed signal is gone with Qt; the facade/event + // layer is responsible for notifying subscribers. + } catch (ocio::Exception &) { + } +} + +} diff --git a/src/node/src/color/colormanager/colormanager.h b/src/node/src/color/colormanager/colormanager.h new file mode 100644 index 000000000..944d56718 --- /dev/null +++ b/src/node/src/color/colormanager/colormanager.h @@ -0,0 +1,105 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_COLORSERVICE_H +#define OAK_COLORSERVICE_H + +#include +#include + +#include "codec/frame.h" +#include "colortransform.h" +#include "node.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +class ColorManager { +public: + ColorManager(Project *project); + + void init(); + + ocio::ConstConfigRcPtr get_config() const; + + static ocio::ConstConfigRcPtr create_config_from_file(const std::string &filename); + + std::string get_config_filename() const; + + static ocio::ConstConfigRcPtr get_default_config(); + + static void set_up_default_config(); + + void set_config_filename(const std::string &filename); + + StringList list_available_displays(); + + std::string get_default_display(); + + StringList list_available_views(std::string display); + + std::string get_default_view(const std::string &display); + + StringList list_available_looks(); + + StringList list_available_colorspaces() const; + + std::string get_default_input_color_space() const; + + /** + * @brief Auto-detects an input colorspace from media color tags + * + * Maps raw FFmpeg color primaries/transfer values (as exposed on + * VideoParams) to a colorspace of the active OCIO config. Returns an + * empty string when the tags are unknown or the config has no matching + * colorspace, in which case the default input colorspace applies. + */ + std::string get_colorspace_for_ffmpeg_tags(int primaries, int trc) const; + + void set_default_input_color_space(const std::string &s); + + std::string get_reference_color_space() const; + + std::string get_compliant_color_space(const std::string &s); + + ColorTransform get_compliant_color_space(const ColorTransform &transform, + bool force_display = false); + + static StringList list_available_colorspaces(ocio::ConstConfigRcPtr config); + + void get_default_luma_coefs(double *rgb) const; + + Project *project() const; + + void update_config_from_filename(); + +private: + Project *project_; + + ocio::ConstConfigRcPtr config_; + + static ocio::ConstConfigRcPtr default_config; +}; + +} + +#endif // OAK_COLORSERVICE_H diff --git a/src/node/src/color/displaytransform/CMakeLists.txt b/src/node/src/color/displaytransform/CMakeLists.txt new file mode 100644 index 000000000..3dd88deee --- /dev/null +++ b/src/node/src/color/displaytransform/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/displaytransform/displaytransform.cpp + node/color/displaytransform/displaytransform.h + PARENT_SCOPE +) diff --git a/src/node/src/color/displaytransform/displaytransform.cpp b/src/node/src/color/displaytransform/displaytransform.cpp new file mode 100644 index 000000000..7a0f5ebd5 --- /dev/null +++ b/src/node/src/color/displaytransform/displaytransform.cpp @@ -0,0 +1,156 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "displaytransform.h" + +#include "color/colormanager/colormanager.h" + +namespace olive +{ + +const std::string DisplayTransformNode::k_display_input = "display_in"; +const std::string DisplayTransformNode::k_view_input = "view_in"; +const std::string DisplayTransformNode::k_direction_input = "dir_in"; + +#define super OCIOBaseNode + +DisplayTransformNode::DisplayTransformNode() +{ + add_input(k_display_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + + add_input(k_view_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + + add_input(k_direction_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); +} + +std::string DisplayTransformNode::name() const +{ + return "Display Transform"; +} + +std::string DisplayTransformNode::id() const +{ + return "org.olivevideoeditor.Olive.displaytransform"; +} + +std::vector DisplayTransformNode::category() const +{ + return { k_category_color }; +} + +std::string DisplayTransformNode::description() const +{ + return "Converts an image to or from a display color space."; +} + +void DisplayTransformNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_display_input, "Display"); + set_input_name(k_view_input, "View"); + set_input_name(k_direction_input, "Direction"); + set_combo_box_strings(k_direction_input, { "Forward", "Inverse" }); +} + +void DisplayTransformNode::InputValueChangedEvent(const std::string &input, + int element) +{ + (void) element; + if (input == k_display_input || input == k_direction_input || + input == k_view_input) { + if (input == k_display_input) { + update_views(); + } + generate_processor(); + } +} + +std::string DisplayTransformNode::get_display() const +{ + if (manager()) { + int index = get_standard_value(k_display_input).to_int(); + if (index < int(manager()->list_available_displays().size())) { + return manager()->list_available_displays().at(index); + } + } + return std::string(); +} + +std::string DisplayTransformNode::get_view() const +{ + if (manager()) { + std::string display = get_display(); + if (!display.empty()) { + int index = get_standard_value(k_view_input).to_int(); + StringList views = manager()->list_available_views(display); + if (index < int(views.size())) { + return views.at(index); + } + } + } + return std::string(); +} + +ColorProcessor::Direction DisplayTransformNode::get_direction() const +{ + return static_cast( + get_standard_value(k_direction_input).to_int()); + ; +} + +void DisplayTransformNode::update_displays() +{ + if (manager()) { + set_combo_box_strings(k_display_input, manager()->list_available_displays()); + } +} + +void DisplayTransformNode::update_views() +{ + if (manager()) { + set_combo_box_strings(k_view_input, + manager()->list_available_views(get_display())); + } +} + +void DisplayTransformNode::config_changed() +{ + update_displays(); + update_views(); + generate_processor(); +} + +void DisplayTransformNode::generate_processor() +{ + if (manager()) { + ColorTransform transform(get_display(), get_view(), std::string()); + set_processor(ColorProcessor::create( + manager(), manager()->get_reference_color_space(), transform, + get_direction())); + } +} + +} diff --git a/src/node/src/color/displaytransform/displaytransform.h b/src/node/src/color/displaytransform/displaytransform.h new file mode 100644 index 000000000..27c7e91ab --- /dev/null +++ b/src/node/src/color/displaytransform/displaytransform.h @@ -0,0 +1,67 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DISPLAYTRANSFORMNODE_H +#define OAK_DISPLAYTRANSFORMNODE_H + +#include "color/ociobase/ociobase.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +class DisplayTransformNode : public OCIOBaseNode { +public: + DisplayTransformNode(); + + NODE_DEFAULT_FUNCTIONS(DisplayTransformNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + + std::string get_display() const; + std::string get_view() const; + ColorProcessor::Direction get_direction() const; + + static const std::string k_display_input; + static const std::string k_view_input; + static const std::string k_direction_input; + +protected: + virtual void config_changed() override; + +private: + void generate_processor(); + + void update_displays(); + + void update_views(); +}; + +} // olive + +#endif // OAK_DISPLAYTRANSFORMNODE_H diff --git a/src/node/src/color/ociobase/CMakeLists.txt b/src/node/src/color/ociobase/CMakeLists.txt new file mode 100644 index 000000000..5bacc8f18 --- /dev/null +++ b/src/node/src/color/ociobase/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/ociobase/ociobase.cpp + node/color/ociobase/ociobase.h + PARENT_SCOPE +) diff --git a/src/node/src/color/ociobase/ociobase.cpp b/src/node/src/color/ociobase/ociobase.cpp new file mode 100644 index 000000000..a6893a0cf --- /dev/null +++ b/src/node/src/color/ociobase/ociobase.cpp @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ociobase.h" + +#include "color/colormanager/colormanager.h" +#include "project.h" + +namespace olive +{ + +const std::string OCIOBaseNode::k_texture_input = "tex_in"; + +OCIOBaseNode::OCIOBaseNode() + : manager_(nullptr) + , processor_(nullptr) +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + set_effect_input(k_texture_input); + + set_flag(k_video_effect); +} + +void OCIOBaseNode::AddedToGraphEvent(Project *p) +{ + manager_ = p->color_manager(); + config_changed(); +} + +void OCIOBaseNode::RemovedFromGraphEvent(Project *p) +{ + if (manager_) { + manager_ = nullptr; + } +} + +void OCIOBaseNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + auto tex_met = value.at(k_texture_input); + TexturePtr t = tex_met.to_texture(); + if (t) { + if (processor_) { + ColorTransformJob job; + + job.set_color_processor(processor_); + job.set_input_texture(tex_met); + + table->push(NodeValue::k_texture, t->to_job(job), this); + } else { + // Processor isn't ready yet (e.g. still being generated + // asynchronously), pass the input through unchanged. + table->push(NodeValue::k_texture, Variant::from_value(t), this); + } + } +} + +} diff --git a/src/node/src/color/ociobase/ociobase.h b/src/node/src/color/ociobase/ociobase.h new file mode 100644 index 000000000..d5ab58982 --- /dev/null +++ b/src/node/src/color/ociobase/ociobase.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OCIOBASENODE_H +#define OAK_OCIOBASENODE_H + +#include "node.h" +#include "render/job/colortransformjob.h" + +namespace olive +{ + +class OCIOBaseNode : public Node { +public: + OCIOBaseNode(); + + virtual void AddedToGraphEvent(Project *p) override; + virtual void RemovedFromGraphEvent(Project *p) override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + +protected: + // Called when the OCIO config changes. The former ColorManager::config_changed + // signal connection is gone with Qt; the facade/event layer invokes this. + virtual void config_changed() = 0; + +protected: + ColorManager *manager() const + { + return manager_; + } + + ColorProcessorPtr processor() const + { + return processor_; + } + void set_processor(ColorProcessorPtr p) + { + processor_ = p; + } + +private: + ColorManager *manager_; + + ColorProcessorPtr processor_; +}; + +} + +#endif // OAK_OCIOBASENODE_H diff --git a/src/node/src/color/ociogradingtransformlinear/CMakeLists.txt b/src/node/src/color/ociogradingtransformlinear/CMakeLists.txt new file mode 100644 index 000000000..78053df26 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlinear/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/ociogradingtransformlinear/ociogradingtransformlinear.cpp + node/color/ociogradingtransformlinear/ociogradingtransformlinear.h + PARENT_SCOPE +) diff --git a/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.cpp b/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.cpp new file mode 100644 index 000000000..ea7935023 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.cpp @@ -0,0 +1,300 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ociogradingtransformlinear.h" + +#include +#include + +#include "ocioutils.h" +#include "project.h" +#include "render/colorprocessor.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string OCIOGradingTransformLinearNode::k_contrast_input = + "ocio_grading_primary_contrast"; +const std::string OCIOGradingTransformLinearNode::k_offset_input = + "ocio_grading_primary_offset"; +const std::string OCIOGradingTransformLinearNode::k_exposure_input = + "ocio_grading_primary_exposure"; +const std::string OCIOGradingTransformLinearNode::k_saturation_input = + "ocio_grading_primary_saturation"; +const std::string OCIOGradingTransformLinearNode::k_pivot_input = + "ocio_grading_primary_pivot"; +const std::string OCIOGradingTransformLinearNode::k_clamp_black_enable_input = + "clamp_black_enable_in"; +const std::string OCIOGradingTransformLinearNode::k_clamp_black_input = + "ocio_grading_primary_clampBlack"; +const std::string OCIOGradingTransformLinearNode::k_clamp_white_enable_input = + "clamp_white_enable_in"; +const std::string OCIOGradingTransformLinearNode::k_clamp_white_input = + "ocio_grading_primary_clampWhite"; + +#define super OCIOBaseNode + +OCIOGradingTransformLinearNode::OCIOGradingTransformLinearNode() +{ + add_input(k_contrast_input, NodeValue::k_vec4, Vector4D{ 1.0, 1.0, 1.0, 1.0 }); + // Minimum based on ocio::GradingPrimary::validate + set_input_property(k_contrast_input, "min", + Vector4D{ 0.01f, 0.01f, 0.01f, 0.01f }); + set_input_property(k_contrast_input, "base", 0.01); + set_vec4_input_colors(k_contrast_input); + + add_input(k_offset_input, NodeValue::k_vec4, Vector4D{ 0.0, 0.0, 0.0, 0.0 }); + set_input_property(k_offset_input, "base", 0.01); + set_vec4_input_colors(k_offset_input); + + add_input(k_exposure_input, NodeValue::k_vec4, Vector4D{ 0.0, 0.0, 0.0, 0.0 }); + set_input_property(k_exposure_input, "base", 0.01); + set_vec4_input_colors(k_exposure_input); + + add_input(k_saturation_input, NodeValue::k_float, 1.0); + set_input_property(k_saturation_input, "view", slider::k_percentage); + set_input_property(k_saturation_input, "min", 0.0); + + add_input(k_pivot_input, NodeValue::k_float, + 0.18); // Default listed in ocio::GradingPrimary + set_input_property(k_pivot_input, "base", 0.01); + + add_input(k_clamp_black_enable_input, NodeValue::k_boolean, false); + + add_input(k_clamp_black_input, NodeValue::k_float, 0.0); + set_input_property(k_clamp_black_input, "enabled", + get_standard_value(k_clamp_black_enable_input).to_bool()); + set_input_property(k_clamp_black_input, "base", 0.01); + + add_input(k_clamp_white_enable_input, NodeValue::k_boolean, false); + + add_input(k_clamp_white_input, NodeValue::k_float, 1.0); + set_input_property(k_clamp_white_input, "enabled", + get_standard_value(k_clamp_white_enable_input).to_bool()); + set_input_property(k_clamp_white_input, "base", 0.01); + + // Constrain the white clamp minimum to just above the (static) black clamp + // as per ocio::GradingPrimary::validate. When the black clamp is keyframed + // or connected, Value() enforces the invariant per frame instead. + update_clamp_white_minimum(); +} + +std::string OCIOGradingTransformLinearNode::name() const +{ + return "OCIO Color Grading (Linear)"; +} + +std::string OCIOGradingTransformLinearNode::id() const +{ + return "org.olivevideoeditor.Olive.ociogradingtransformlinear"; +} + +std::vector OCIOGradingTransformLinearNode::category() const +{ + return { k_category_color }; +} + +std::string OCIOGradingTransformLinearNode::description() const +{ + return "Simple linear color grading using OpenColorIO."; +} + +void OCIOGradingTransformLinearNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_contrast_input, "Contrast"); + set_input_name(k_offset_input, "Offset"); + set_input_name(k_exposure_input, "Exposure"); + set_input_property(k_exposure_input, "tooltip", + "Exposure increments in stops."); + set_input_name(k_saturation_input, "Saturation"); + set_input_name(k_pivot_input, "Pivot"); + set_input_name(k_clamp_black_enable_input, "Enable Black Clamp"); + set_input_name(k_clamp_black_input, "Black Clamp"); + set_input_name(k_clamp_white_enable_input, "Enable White Clamp"); + set_input_name(k_clamp_white_input, "White Clamp"); +} + +void OCIOGradingTransformLinearNode::InputValueChangedEvent( + const std::string &input, int element) +{ + (void) element; + + if (input == k_clamp_white_enable_input) { + set_input_property(k_clamp_white_input, "enabled", + get_standard_value(k_clamp_white_enable_input).to_bool()); + } else if (input == k_clamp_black_enable_input) { + set_input_property(k_clamp_black_input, "enabled", + get_standard_value(k_clamp_black_enable_input).to_bool()); + } else if (input == k_clamp_black_input) { + // Ensure the white clamp is always greater than the black clamp as per + // ocio::GradingPrimary::validate + update_clamp_white_minimum(); + } + + generate_processor(); +} + +void OCIOGradingTransformLinearNode::InputConnectedEvent(const std::string &input, + int element, Node *output) +{ + super::InputConnectedEvent(input, element, output); + + if (input == k_clamp_black_input) { + update_clamp_white_minimum(); + } +} + +void OCIOGradingTransformLinearNode::InputDisconnectedEvent(const std::string &input, + int element, + Node *output) +{ + super::InputDisconnectedEvent(input, element, output); + + if (input == k_clamp_black_input) { + update_clamp_white_minimum(); + } +} + +void OCIOGradingTransformLinearNode::update_clamp_white_minimum() +{ + // A static UI minimum cannot follow an animated black clamp; for keyframed + // or connected values the white>black invariant is enforced per frame in + // Value() instead + if (is_input_keyframing(k_clamp_black_input) || + is_input_connected(k_clamp_black_input)) { + return; + } + + set_input_property(k_clamp_white_input, "min", + get_standard_value(k_clamp_black_input).to_double() + 0.000001); +} + +void OCIOGradingTransformLinearNode::generate_processor() +{ + if (manager()) { + ocio::GradingPrimaryTransformRcPtr gp = + ocio::GradingPrimaryTransform::Create(ocio::GRADING_LIN); + gp->makeDynamic(); + gp->setDirection(ocio::TransformDirection::TRANSFORM_DIR_FORWARD); + + try { + set_processor(ColorProcessor::create( + manager()->get_config()->getProcessor(gp))); + } catch (const ocio::Exception &e) { + std::cerr << std::endl << e.what() << std::endl; + } + } +} + +void OCIOGradingTransformLinearNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + if (processor()) { + ColorTransformJob job(value); + + job.set_color_processor(processor()); + job.set_input_texture(value.at(k_texture_input)); + + // Vector4D components stand in for the former QVector4D indices: + // x = master channel, y = red, z = green, w = blue. + + // Oddly, OCIO uses RGBMs when setting the GradingPrimary on the CPU, but uses vec3s on the GPU. + // Even more oddly, the conversion from RGBM to vec3 does not appear to have a public API. + // Therefore, this code has been duplicated from OCIO here: + // https://github.com/AcademySoftwareFoundation/OpenColorIO/blob/3abbe5b20521169580fcfe3692aca81859859953/src/OpenColorIO/ops/gradingprimary/GradingPrimary.cpp#L157 + Vector4D offset = value.at(k_offset_input).to_vec4(); + offset.set_y(offset.y() + offset.x()); + offset.set_z(offset.z() + offset.x()); + offset.set_w(offset.w() + offset.x()); + job.insert(k_offset_input, + NodeValue(NodeValue::k_vec3, + Vector3D(offset.y(), offset.z(), offset.w()))); + + Vector4D exposure = value.at(k_exposure_input).to_vec4(); + exposure.set_y(std::pow(2.0f, exposure.x() + exposure.y())); + exposure.set_z(std::pow(2.0f, exposure.x() + exposure.z())); + exposure.set_w(std::pow(2.0f, exposure.x() + exposure.w())); + job.insert(k_exposure_input, + NodeValue(NodeValue::k_vec3, + Vector3D(exposure.y(), exposure.z(), + exposure.w()))); + + Vector4D contrast = value.at(k_contrast_input).to_vec4(); + contrast.set_y(contrast.y() * contrast.x()); + contrast.set_z(contrast.z() * contrast.x()); + contrast.set_w(contrast.w() * contrast.x()); + job.insert(k_contrast_input, + NodeValue(NodeValue::k_vec3, + Vector3D(contrast.y(), contrast.z(), + contrast.w()))); + + if (!value.at(k_clamp_black_enable_input).to_bool()) { + job.insert(k_clamp_black_input, + NodeValue(NodeValue::k_float, + ocio::GradingPrimary::NoClampBlack())); + } + + if (!value.at(k_clamp_white_enable_input).to_bool()) { + job.insert(k_clamp_white_input, + NodeValue(NodeValue::k_float, + ocio::GradingPrimary::NoClampWhite())); + } + + if (value.at(k_clamp_black_enable_input).to_bool() && + value.at(k_clamp_white_enable_input).to_bool()) { + // ocio::GradingPrimary::validate requires the white clamp to be + // greater than the black clamp. Keyframed or connected values + // can violate this at arbitrary times, so enforce the invariant + // per frame here. + const double clamp_black = value.at(k_clamp_black_input).to_double(); + const double clamp_white = value.at(k_clamp_white_input).to_double(); + if (clamp_white <= clamp_black) { + job.insert(k_clamp_white_input, + NodeValue(NodeValue::k_float, + clamp_black + 0.000001)); + } + } + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } + } +} + +void OCIOGradingTransformLinearNode::config_changed() +{ + generate_processor(); +} + +void OCIOGradingTransformLinearNode::set_vec4_input_colors(const std::string &input) +{ + set_input_property(input, "color0", "#c0c0c0"); + set_input_property(input, "color1", "#ff0000"); + set_input_property(input, "color2", "#00ff00"); + set_input_property(input, "color3", "#0000ff"); +} + +} diff --git a/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.h b/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.h new file mode 100644 index 000000000..a5fe2fa87 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlinear/ociogradingtransformlinear.h @@ -0,0 +1,83 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OCIOGRADINGTRANSFORMLINEARNODE_H +#define OAK_OCIOGRADINGTRANSFORMLINEARNODE_H + +#include "color/ociobase/ociobase.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +class OCIOGradingTransformLinearNode : public OCIOBaseNode { +public: + OCIOGradingTransformLinearNode(); + + NODE_DEFAULT_FUNCTIONS(OCIOGradingTransformLinearNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + void generate_processor(); + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_contrast_input; + static const std::string k_offset_input; + static const std::string k_exposure_input; + static const std::string k_saturation_input; + static const std::string k_pivot_input; + static const std::string k_clamp_black_enable_input; + static const std::string k_clamp_black_input; + static const std::string k_clamp_white_enable_input; + static const std::string k_clamp_white_input; + +protected: + virtual void config_changed() override; + +private: + void set_vec4_input_colors(const std::string &input); + + /** + * @brief Constrains the white clamp UI minimum to just above the black + * clamp, as required by ocio::GradingPrimary::validate + * + * Only applies while the black clamp is a static value; when it is + * keyframed or connected the invariant is enforced per frame in Value() + * instead. + */ + void update_clamp_white_minimum(); +}; + +} // olive + +#endif diff --git a/src/node/src/color/ociogradingtransformlog/CMakeLists.txt b/src/node/src/color/ociogradingtransformlog/CMakeLists.txt new file mode 100644 index 000000000..57ace5e47 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlog/CMakeLists.txt @@ -0,0 +1,23 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# Modifications Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/ociogradingtransformlog/ociogradingtransformlog.cpp + node/color/ociogradingtransformlog/ociogradingtransformlog.h + PARENT_SCOPE +) diff --git a/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp new file mode 100644 index 000000000..5cbc8a4b8 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp @@ -0,0 +1,295 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ociogradingtransformlog.h" + +#include +#include + +#include "ocioutils.h" +#include "project.h" +#include "render/colorprocessor.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +// These ids double as the OCIO GPU uniform names for the dynamic +// GradingPrimaryTransform; do not rename them. OCIO's log style maps the +// classic wheels as: brightness = lift, contrast = gain, gamma = gamma. +const std::string OCIOGradingTransformLogNode::k_lift_input = + "ocio_grading_primary_brightness"; +const std::string OCIOGradingTransformLogNode::k_gain_input = + "ocio_grading_primary_contrast"; +const std::string OCIOGradingTransformLogNode::k_gamma_input = + "ocio_grading_primary_gamma"; +const std::string OCIOGradingTransformLogNode::k_saturation_input = + "ocio_grading_primary_saturation"; +const std::string OCIOGradingTransformLogNode::k_pivot_input = + "ocio_grading_primary_pivot"; +const std::string OCIOGradingTransformLogNode::k_clamp_black_enable_input = + "clamp_black_enable_in"; +const std::string OCIOGradingTransformLogNode::k_clamp_black_input = + "ocio_grading_primary_clampBlack"; +const std::string OCIOGradingTransformLogNode::k_clamp_white_enable_input = + "clamp_white_enable_in"; +const std::string OCIOGradingTransformLogNode::k_clamp_white_input = + "ocio_grading_primary_clampWhite"; + +#define super OCIOBaseNode + +OCIOGradingTransformLogNode::OCIOGradingTransformLogNode() +{ + add_input(k_lift_input, NodeValue::k_vec4, Vector4D{ 0.0, 0.0, 0.0, 0.0 }); + set_input_property(k_lift_input, "base", 0.01); + set_vec4_input_colors(k_lift_input); + + add_input(k_gain_input, NodeValue::k_vec4, Vector4D{ 1.0, 1.0, 1.0, 1.0 }); + set_input_property(k_gain_input, "base", 0.01); + set_vec4_input_colors(k_gain_input); + + add_input(k_gamma_input, NodeValue::k_vec4, Vector4D{ 1.0, 1.0, 1.0, 1.0 }); + set_input_property(k_gamma_input, "base", 0.01); + set_vec4_input_colors(k_gamma_input); + + add_input(k_saturation_input, NodeValue::k_float, 1.0); + set_input_property(k_saturation_input, "view", slider::k_percentage); + set_input_property(k_saturation_input, "min", 0.0); + + add_input(k_pivot_input, NodeValue::k_float, + -0.2); // Default for GRADING_LOG listed in ocio::GradingPrimary + set_input_property(k_pivot_input, "base", 0.01); + + add_input(k_clamp_black_enable_input, NodeValue::k_boolean, false); + + add_input(k_clamp_black_input, NodeValue::k_float, 0.0); + set_input_property(k_clamp_black_input, "enabled", + get_standard_value(k_clamp_black_enable_input).to_bool()); + set_input_property(k_clamp_black_input, "base", 0.01); + + add_input(k_clamp_white_enable_input, NodeValue::k_boolean, false); + + add_input(k_clamp_white_input, NodeValue::k_float, 1.0); + set_input_property(k_clamp_white_input, "enabled", + get_standard_value(k_clamp_white_enable_input).to_bool()); + set_input_property(k_clamp_white_input, "base", 0.01); + + // Constrain the white clamp minimum to just above the (static) black clamp + // as per ocio::GradingPrimary::validate. When the black clamp is keyframed + // or connected, Value() enforces the invariant per frame instead. + update_clamp_white_minimum(); +} + +std::string OCIOGradingTransformLogNode::name() const +{ + return "OCIO Color Grading (Log)"; +} + +std::string OCIOGradingTransformLogNode::id() const +{ + return "org.olivevideoeditor.Olive.ociogradingtransformlog"; +} + +std::vector OCIOGradingTransformLogNode::category() const +{ + return { k_category_color }; +} + +std::string OCIOGradingTransformLogNode::description() const +{ + return "Lift/gamma/gain color grading using OpenColorIO."; +} + +void OCIOGradingTransformLogNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_lift_input, "Lift"); + set_input_name(k_gain_input, "Gain"); + set_input_name(k_gamma_input, "Gamma"); + set_input_name(k_saturation_input, "Saturation"); + set_input_name(k_pivot_input, "Pivot"); + set_input_name(k_clamp_black_enable_input, "Enable Black Clamp"); + set_input_name(k_clamp_black_input, "Black Clamp"); + set_input_name(k_clamp_white_enable_input, "Enable White Clamp"); + set_input_name(k_clamp_white_input, "White Clamp"); +} + +void OCIOGradingTransformLogNode::InputValueChangedEvent(const std::string &input, + int element) +{ + (void) element; + + if (input == k_clamp_white_enable_input) { + set_input_property(k_clamp_white_input, "enabled", + get_standard_value(k_clamp_white_enable_input).to_bool()); + } else if (input == k_clamp_black_enable_input) { + set_input_property(k_clamp_black_input, "enabled", + get_standard_value(k_clamp_black_enable_input).to_bool()); + } else if (input == k_clamp_black_input) { + // Ensure the white clamp is always greater than the black clamp as per + // ocio::GradingPrimary::validate + update_clamp_white_minimum(); + } + + generate_processor(); +} + +void OCIOGradingTransformLogNode::InputConnectedEvent(const std::string &input, + int element, Node *output) +{ + super::InputConnectedEvent(input, element, output); + + if (input == k_clamp_black_input) { + update_clamp_white_minimum(); + } +} + +void OCIOGradingTransformLogNode::InputDisconnectedEvent(const std::string &input, + int element, + Node *output) +{ + super::InputDisconnectedEvent(input, element, output); + + if (input == k_clamp_black_input) { + update_clamp_white_minimum(); + } +} + +void OCIOGradingTransformLogNode::update_clamp_white_minimum() +{ + // A static UI minimum cannot follow an animated black clamp; for keyframed + // or connected values the white>black invariant is enforced per frame in + // Value() instead + if (is_input_keyframing(k_clamp_black_input) || + is_input_connected(k_clamp_black_input)) { + return; + } + + set_input_property(k_clamp_white_input, "min", + get_standard_value(k_clamp_black_input).to_double() + 0.000001); +} + +void OCIOGradingTransformLogNode::generate_processor() +{ + if (manager()) { + ocio::GradingPrimaryTransformRcPtr gp = + ocio::GradingPrimaryTransform::Create(ocio::GRADING_LOG); + gp->makeDynamic(); + gp->setDirection(ocio::TransformDirection::TRANSFORM_DIR_FORWARD); + + try { + set_processor(ColorProcessor::create( + manager()->get_config()->getProcessor(gp))); + } catch (const ocio::Exception &e) { + std::cerr << std::endl << e.what() << std::endl; + } + } +} + +void OCIOGradingTransformLogNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + if (processor()) { + ColorTransformJob job(value); + + job.set_color_processor(processor()); + job.set_input_texture(value.at(k_texture_input)); + + // Vector4D components stand in for the former QVector4D indices: + // x = master channel, y = red, z = green, w = blue. + + // OCIO expects vec3s on the GPU but RGBMs (master + RGB) on the + // CPU; the per-style master combination below mirrors + // ocio::GradingPrimary. Lift is additive, gain/gamma multiply. + Vector4D lift = value.at(k_lift_input).to_vec4(); + lift.set_y(lift.y() + lift.x()); + lift.set_z(lift.z() + lift.x()); + lift.set_w(lift.w() + lift.x()); + job.insert(k_lift_input, + NodeValue(NodeValue::k_vec3, + Vector3D(lift.y(), lift.z(), lift.w()))); + + Vector4D gain = value.at(k_gain_input).to_vec4(); + gain.set_y(gain.y() * gain.x()); + gain.set_z(gain.z() * gain.x()); + gain.set_w(gain.w() * gain.x()); + job.insert(k_gain_input, + NodeValue(NodeValue::k_vec3, + Vector3D(gain.y(), gain.z(), gain.w()))); + + Vector4D gamma = value.at(k_gamma_input).to_vec4(); + gamma.set_y(gamma.y() * gamma.x()); + gamma.set_z(gamma.z() * gamma.x()); + gamma.set_w(gamma.w() * gamma.x()); + job.insert(k_gamma_input, + NodeValue(NodeValue::k_vec3, + Vector3D(gamma.y(), gamma.z(), gamma.w()))); + + if (!value.at(k_clamp_black_enable_input).to_bool()) { + job.insert(k_clamp_black_input, + NodeValue(NodeValue::k_float, + ocio::GradingPrimary::NoClampBlack())); + } + + if (!value.at(k_clamp_white_enable_input).to_bool()) { + job.insert(k_clamp_white_input, + NodeValue(NodeValue::k_float, + ocio::GradingPrimary::NoClampWhite())); + } + + if (value.at(k_clamp_black_enable_input).to_bool() && + value.at(k_clamp_white_enable_input).to_bool()) { + // ocio::GradingPrimary::validate requires the white clamp to be + // greater than the black clamp. Keyframed or connected values + // can violate this at arbitrary times, so enforce the invariant + // per frame here. + const double clamp_black = value.at(k_clamp_black_input).to_double(); + const double clamp_white = value.at(k_clamp_white_input).to_double(); + if (clamp_white <= clamp_black) { + job.insert(k_clamp_white_input, + NodeValue(NodeValue::k_float, + clamp_black + 0.000001)); + } + } + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } + } +} + +void OCIOGradingTransformLogNode::config_changed() +{ + generate_processor(); +} + +void OCIOGradingTransformLogNode::set_vec4_input_colors(const std::string &input) +{ + set_input_property(input, "color0", "#c0c0c0"); + set_input_property(input, "color1", "#ff0000"); + set_input_property(input, "color2", "#00ff00"); + set_input_property(input, "color3", "#0000ff"); +} + +} diff --git a/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.h b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.h new file mode 100644 index 000000000..a5f1e3f26 --- /dev/null +++ b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.h @@ -0,0 +1,90 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OCIOGRADINGTRANSFORMLOGNODE_H +#define OAK_OCIOGRADINGTRANSFORMLOGNODE_H + +#include "color/ociobase/ociobase.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +/** + * @brief Lift/gamma/gain grading node built on ocio::GRADING_LOG + * + * Mirrors OCIOGradingTransformLinearNode for the log grading style. OCIO's + * log-style GPU uniforms map to the classic wheels as: brightness = lift, + * contrast = gain, gamma = gamma. + */ +class OCIOGradingTransformLogNode : public OCIOBaseNode { +public: + OCIOGradingTransformLogNode(); + + NODE_DEFAULT_FUNCTIONS(OCIOGradingTransformLogNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + void generate_processor(); + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_lift_input; + static const std::string k_gain_input; + static const std::string k_gamma_input; + static const std::string k_saturation_input; + static const std::string k_pivot_input; + static const std::string k_clamp_black_enable_input; + static const std::string k_clamp_black_input; + static const std::string k_clamp_white_enable_input; + static const std::string k_clamp_white_input; + +protected: + virtual void config_changed() override; + +private: + void set_vec4_input_colors(const std::string &input); + + /** + * @brief Constrains the white clamp UI minimum to just above the black + * clamp, as required by ocio::GradingPrimary::validate + * + * Only applies while the black clamp is a static value; when it is + * keyframed or connected the invariant is enforced per frame in Value() + * instead. + */ + void update_clamp_white_minimum(); +}; + +} // olive + +#endif diff --git a/src/node/src/color/ociolut/CMakeLists.txt b/src/node/src/color/ociolut/CMakeLists.txt new file mode 100644 index 000000000..7acbf9f72 --- /dev/null +++ b/src/node/src/color/ociolut/CMakeLists.txt @@ -0,0 +1,14 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/ociolut/ociolut.cpp + node/color/ociolut/ociolut.h + PARENT_SCOPE +) diff --git a/src/node/src/color/ociolut/ociolut.cpp b/src/node/src/color/ociolut/ociolut.cpp new file mode 100644 index 000000000..918cfad8e --- /dev/null +++ b/src/node/src/color/ociolut/ociolut.cpp @@ -0,0 +1,315 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ociolut.h" + +#include +#include +#include +#include +#include + +#include "color/colormanager/colormanager.h" +#include "render/lutlibrary.h" +#include "render/previewautocacher.h" +#include "render/rendermanager.h" + +namespace olive +{ + +const std::string OCIOLutNode::k_file_input = "lut_file_in"; +const std::string OCIOLutNode::k_direction_input = "lut_dir_in"; + +#define super OCIOBaseNode + +namespace +{ + +bool is_main_process() +{ + // Qt-free replacement for qobject_cast(QCoreApplication::instance()): + // only the main GUI process creates a RenderManager (the render worker + // never does), so its presence identifies the main process. + return RenderManager::instance() != nullptr; +} + +int read_direction_input(const Node *node) +{ + Variant v = node->get_standard_value(OCIOLutNode::k_direction_input); + + bool ok = false; + int direction = v.to_int(&ok); + if (ok) { + return direction; + } + + // Some old serializers stored the combo value as a string. + std::string s = v.to_string(); + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (s == "forward" || s == "0") { + return 0; + } + if (s == "inverse" || s == "1") { + return 1; + } + + fprintf(stderr, "OCIOLutNode: unexpected direction value %s\n", + v.to_string().c_str()); + return 0; +} + +} // namespace + +OCIOLutNode::OCIOLutNode() +{ + add_input(k_file_input, NodeValue::k_file, std::string(), + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + const StringList &extensions = LUTLibrary::supported_extensions(); + std::string all_luts = "*."; + for (size_t i = 0; i < extensions.size(); i++) { + if (i > 0) { + all_luts += " *."; + } + all_luts += extensions[i]; + } + set_input_property(k_file_input, "filter", + "LUT Files (" + all_luts + ");;All Files (*)"); + set_input_property(k_file_input, "placeholder", "Select a LUT file"); + // Allow the UI to offer the global LUT library for this input + set_input_property(k_file_input, "lut_library", true); + + add_input(k_direction_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); +} + +std::string OCIOLutNode::name() const +{ + return "OCIO LUT"; +} + +std::string OCIOLutNode::id() const +{ + return "org.olivevideoeditor.Olive.ociolut"; +} + +std::vector OCIOLutNode::category() const +{ + return { k_category_color }; +} + +std::string OCIOLutNode::description() const +{ + return "Applies a LUT file through OpenColorIO."; +} + +void OCIOLutNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_file_input, "LUT File"); + set_input_name(k_direction_input, "Direction"); + set_combo_box_strings(k_direction_input, { "Forward", "Inverse" }); +} + +void OCIOLutNode::InputValueChangedEvent(const std::string &input, int element) +{ + (void) element; + + if (input == k_file_input || input == k_direction_input) { + // In the worker process, creating the OCIO processor can be slow and we + // are often called from LoadGraph while the main process is blocked + // waiting for a response. Defer generation to Value() time so the worker + // can ack the graph load immediately. + if (is_main_process()) { + generate_processor(); + } else { + std::lock_guard locker(gen_mutex_); + processor_dirty_ = true; + } + } +} + +void OCIOLutNode::config_changed() +{ + if (is_main_process()) { + generate_processor(); + } else { + std::lock_guard locker(gen_mutex_); + processor_dirty_ = true; + } +} + +void OCIOLutNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Ensure the processor is up-to-date before the base class emits the color + // transform job. This is especially important in the render worker, where + // processor creation is deferred until the first render. + ensure_processor(); + + super::value(value, globals, table); +} + +void OCIOLutNode::generate_processor() +{ + ensure_processor(); + + // The processor has changed. In the main GUI process, refresh the viewer by + // invalidating the cache and cancelling background cache jobs. + // Invalidating first ensures any in-flight renders that complete afterwards + // won't write stale frames back. The worker process has no + // RenderManager/PreviewAutoCacher, so skip this step to avoid crashing. + if (is_main_process()) { + invalidate_all(k_texture_input); + if (RenderManager *rm = RenderManager::instance()) { + if (PreviewAutoCacher *cacher = rm->get_cacher()) { + cacher->cancel_video_tasks(false); + } + } + } +} + +void OCIOLutNode::ensure_processor() const +{ + std::lock_guard locker(gen_mutex_); + + if (!processor_dirty_ && last_processor_ && + get_standard_value(k_file_input).to_string() == last_path_ && + read_direction_input(this) == last_direction_) { + return; + } + + create_processor_from_inputs(); +} + +void OCIOLutNode::set_last_error(const std::string &error) const +{ + if (last_error_ == error) { + return; + } + + last_error_ = error; + + // The Qt version surfaced the error on the main window status bar through + // EngineCore; that layer is out of oaknode, so the error is now only + // recorded here and surfaced via last_error(). +} + +bool OCIOLutNode::create_processor_from_inputs() const +{ + if (!manager()) { + const_cast(this)->set_processor(nullptr); + last_processor_.reset(); + last_path_.clear(); + last_direction_ = -1; + processor_dirty_ = false; + return false; + } + + const std::string path = get_standard_value(k_file_input).to_string(); + const int direction = read_direction_input(this); + + if (path.empty()) { + const_cast(this)->set_processor(nullptr); + last_processor_.reset(); + last_path_.clear(); + last_direction_ = -1; + processor_dirty_ = false; + set_last_error(std::string()); + return false; + } + + // Re-use the existing processor if the file and direction haven't changed. + if (path == last_path_ && direction == last_direction_ && last_processor_) { + processor_dirty_ = false; + return false; + } + + std::error_code fs_ec; + const bool is_file = + std::filesystem::is_regular_file(path, fs_ec) && !fs_ec; + if (!is_file) { + fprintf(stderr, "OCIO LUT file does not exist: %s\n", path.c_str()); + const_cast(this)->set_processor(nullptr); + last_processor_.reset(); + last_path_.clear(); + last_direction_ = -1; + processor_dirty_ = false; + set_last_error("OCIO LUT: file does not exist: " + path); + return false; + } + + std::string suffix = std::filesystem::path(path).extension().string(); + if (!suffix.empty() && suffix.front() == '.') { + suffix.erase(suffix.begin()); + } + if (!LUTLibrary::is_supported_extension(suffix)) { + fprintf(stderr, "Unsupported OCIO LUT file extension: %s\n", + path.c_str()); + const_cast(this)->set_processor(nullptr); + last_processor_.reset(); + last_path_.clear(); + last_direction_ = -1; + processor_dirty_ = false; + set_last_error("OCIO LUT: unsupported LUT file extension: " + path); + return false; + } + + ColorProcessorPtr processor; + try { + const bool forward = static_cast( + direction) == ColorProcessor::k_normal; + fprintf(stderr, + "OCIOLutNode: creating processor for %s direction=%d " + "ocio_dir=%s process=%s\n", + path.c_str(), direction, forward ? "FORWARD" : "INVERSE", + is_main_process() ? "main" : "worker"); + + ocio::FileTransformRcPtr transform = ocio::FileTransform::Create(); + transform->setSrc(path.c_str()); + transform->setInterpolation(ocio::INTERP_LINEAR); + transform->setDirection(forward ? ocio::TRANSFORM_DIR_FORWARD : + ocio::TRANSFORM_DIR_INVERSE); + + processor = ColorProcessor::create( + manager()->get_config()->getProcessor(transform)); + } catch (const std::exception &e) { + fprintf(stderr, "OCIO LUT processor error: %s\n", e.what()); + processor = nullptr; + } + + if (!processor) { + set_last_error("OCIO LUT: failed to load LUT file: " + path); + } else { + set_last_error(std::string()); + } + + last_path_ = path; + last_direction_ = direction; + last_processor_ = processor; + const_cast(this)->set_processor(processor); + processor_dirty_ = false; + + return true; +} + +} // namespace olive diff --git a/src/node/src/color/ociolut/ociolut.h b/src/node/src/color/ociolut/ociolut.h new file mode 100644 index 000000000..66f3f890e --- /dev/null +++ b/src/node/src/color/ociolut/ociolut.h @@ -0,0 +1,85 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OCIOLUTNODE_H +#define OAK_OCIOLUTNODE_H + +#include +#include + +#include "color/ociobase/ociobase.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +class OCIOLutNode : public OCIOBaseNode { +public: + OCIOLutNode(); + + NODE_DEFAULT_FUNCTIONS(OCIOLutNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_file_input; + static const std::string k_direction_input; + + /** + * @brief Human-readable description of why no LUT processor is active + * + * Empty when a valid LUT processor is in use or no LUT file has been + * selected yet. This allows the UI (and tests) to surface silent + * passthrough states (missing file, unsupported extension, OCIO errors). + */ + const std::string &last_error() const + { + return last_error_; + } + +protected: + virtual void config_changed() override; + +private: + void generate_processor(); + void ensure_processor() const; + bool create_processor_from_inputs() const; + + void set_last_error(const std::string &error) const; + + mutable std::mutex gen_mutex_; + mutable bool processor_dirty_ = true; + mutable std::string last_path_; + mutable int last_direction_ = -1; + mutable ColorProcessorPtr last_processor_; + mutable std::string last_error_; +}; + +} // namespace olive + +#endif // OAK_OCIOLUTNODE_H diff --git a/src/node/src/color/threewaycolor/CMakeLists.txt b/src/node/src/color/threewaycolor/CMakeLists.txt new file mode 100644 index 000000000..f6307f6b2 --- /dev/null +++ b/src/node/src/color/threewaycolor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/threewaycolor/threewaycolor.h + node/color/threewaycolor/threewaycolor.cpp + PARENT_SCOPE +) diff --git a/src/node/src/color/threewaycolor/threewaycolor.cpp b/src/node/src/color/threewaycolor/threewaycolor.cpp new file mode 100644 index 000000000..82ddba599 --- /dev/null +++ b/src/node/src/color/threewaycolor/threewaycolor.cpp @@ -0,0 +1,121 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "threewaycolor.h" + +#include "filefunctions.h" +#include "project.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Node + +const std::string ThreeWayColorNode::k_texture_input = "tex_in"; +const std::string ThreeWayColorNode::k_shadows_color_input = "shadows_color_in"; +const std::string ThreeWayColorNode::k_midtones_color_input = + "midtones_color_in"; +const std::string ThreeWayColorNode::k_highlights_color_input = + "highlights_color_in"; +const std::string ThreeWayColorNode::k_shadows_amount_input = + "shadows_amount_in"; +const std::string ThreeWayColorNode::k_midtones_amount_input = + "midtones_amount_in"; +const std::string ThreeWayColorNode::k_highlights_amount_input = + "highlights_amount_in"; +const std::string ThreeWayColorNode::k_luma_coefficients_input = + "luma_coefficients_in"; + +ThreeWayColorNode::ThreeWayColorNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + const Variant neutral = Variant::from_value(Color(0.5, 0.5, 0.5, 1.0)); + add_input(k_shadows_color_input, NodeValue::k_color, neutral); + add_input(k_midtones_color_input, NodeValue::k_color, neutral); + add_input(k_highlights_color_input, NodeValue::k_color, neutral); + + add_input(k_shadows_amount_input, NodeValue::k_float, 1.0); + add_input(k_midtones_amount_input, NodeValue::k_float, 1.0); + add_input(k_highlights_amount_input, NodeValue::k_float, 1.0); + + const std::string min = "min"; + const std::string view = "view"; + set_input_property(k_shadows_amount_input, min, 0.0); + set_input_property(k_midtones_amount_input, min, 0.0); + set_input_property(k_highlights_amount_input, min, 0.0); + set_input_property(k_shadows_amount_input, view, slider::k_percentage); + set_input_property(k_midtones_amount_input, view, slider::k_percentage); + set_input_property(k_highlights_amount_input, view, slider::k_percentage); + + set_effect_input(k_texture_input); + set_flag(k_video_effect); +} + +void ThreeWayColorNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_shadows_color_input, "Shadows"); + set_input_name(k_midtones_color_input, "Midtones"); + set_input_name(k_highlights_color_input, "Highlights"); + set_input_name(k_shadows_amount_input, "Shadows Amount"); + set_input_name(k_midtones_amount_input, "Midtones Amount"); + set_input_name(k_highlights_amount_input, "Highlights Amount"); +} + +ShaderCode ThreeWayColorNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/threewaycolor.frag")); +} + +void ThreeWayColorNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + (void) globals; + + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + ShaderJob job(value); + + double luma_coeffs[3] = { 0.0, 0.0, 0.0 }; + if (project() && project()->color_manager()) { + project()->color_manager()->get_default_luma_coefs(luma_coeffs); + } else { + luma_coeffs[0] = 0.2126; + luma_coeffs[1] = 0.7152; + luma_coeffs[2] = 0.0722; + } + job.insert(k_luma_coefficients_input, + NodeValue(NodeValue::k_vec3, + Vector3D(luma_coeffs[0], luma_coeffs[1], + luma_coeffs[2]))); + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} + +} diff --git a/src/node/src/color/threewaycolor/threewaycolor.h b/src/node/src/color/threewaycolor/threewaycolor.h new file mode 100644 index 000000000..c37a93ea3 --- /dev/null +++ b/src/node/src/color/threewaycolor/threewaycolor.h @@ -0,0 +1,76 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2026 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_THREEWAYCOLORNODE_H +#define OAK_THREEWAYCOLORNODE_H + +#include "node.h" + +namespace olive +{ + +class ThreeWayColorNode : public Node { +public: + ThreeWayColorNode(); + + NODE_DEFAULT_FUNCTIONS(ThreeWayColorNode) + + virtual std::string name() const override + { + return "Three-Way Color"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.threewaycolor"; + } + + virtual std::vector category() const override + { + return { k_category_color }; + } + + virtual std::string description() const override + { + return "Adjusts shadows, midtones, and highlights separately."; + } + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_shadows_color_input; + static const std::string k_midtones_color_input; + static const std::string k_highlights_color_input; + static const std::string k_shadows_amount_input; + static const std::string k_midtones_amount_input; + static const std::string k_highlights_amount_input; + static const std::string k_luma_coefficients_input; +}; + +} + +#endif // OAK_THREEWAYCOLORNODE_H diff --git a/src/node/src/color/whitebalance/CMakeLists.txt b/src/node/src/color/whitebalance/CMakeLists.txt new file mode 100644 index 000000000..006e8df15 --- /dev/null +++ b/src/node/src/color/whitebalance/CMakeLists.txt @@ -0,0 +1,22 @@ +# Oak - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/color/whitebalance/whitebalance.cpp + node/color/whitebalance/whitebalance.h + PARENT_SCOPE +) diff --git a/src/node/src/color/whitebalance/whitebalance.cpp b/src/node/src/color/whitebalance/whitebalance.cpp new file mode 100644 index 000000000..07d6aad96 --- /dev/null +++ b/src/node/src/color/whitebalance/whitebalance.cpp @@ -0,0 +1,155 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "whitebalance.h" + +#include +#include + +#include "filefunctions.h" +#include "render/job/shaderjob.h" +#include "render/texture.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Node + +const std::string WhiteBalanceNode::k_texture_input = "tex_in"; +const std::string WhiteBalanceNode::k_temperature_input = "temperature_in"; +const std::string WhiteBalanceNode::k_tint_input = "tint_in"; +const std::string WhiteBalanceNode::k_gain_input = "wb_gain_in"; + +WhiteBalanceNode::WhiteBalanceNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_temperature_input, NodeValue::k_float, 6500.0); + set_input_property(k_temperature_input, "min", 1000.0); + set_input_property(k_temperature_input, "max", 40000.0); + set_input_property(k_temperature_input, "view", slider::k_normal); + + add_input(k_tint_input, NodeValue::k_float, 0.0); + set_input_property(k_tint_input, "min", -1.0); + set_input_property(k_tint_input, "max", 1.0); + set_input_property(k_tint_input, "base", 0.01); + + set_effect_input(k_texture_input); + set_flag(k_video_effect); +} + +std::string WhiteBalanceNode::name() const +{ + return "White Balance"; +} + +std::string WhiteBalanceNode::id() const +{ + return "org.olivevideoeditor.Olive.whitebalance"; +} + +std::vector WhiteBalanceNode::category() const +{ + return { k_category_color }; +} + +std::string WhiteBalanceNode::description() const +{ + return "Adjust white balance by color temperature and tint."; +} + +void WhiteBalanceNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_temperature_input, "Temperature (K)"); + set_input_name(k_tint_input, "Tint"); +} + +ShaderCode WhiteBalanceNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/whitebalance.frag")); +} + +void WhiteBalanceNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + (void) globals; + + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + ShaderJob job(value); + + const Vector3D gain = get_gain_for_temperature( + value.at(k_temperature_input).to_double(), + value.at(k_tint_input).to_double()); + job.insert(k_gain_input, NodeValue(NodeValue::k_vec3, gain)); + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} + +Vector3D WhiteBalanceNode::get_gain_for_temperature(double kelvin, double tint) +{ + // Tanner Helland blackbody approximation (1000K-40000K), returning + // 0-255 per channel + kelvin = std::max(1000.0, std::min(kelvin, 40000.0)); + const double t = kelvin / 100.0; + + double red; + if (t <= 66.0) { + red = 255.0; + } else { + red = 329.698727446 * std::pow(t - 60.0, -0.1332047592); + } + + double green; + if (t <= 66.0) { + green = 99.4708025861 * std::log(t) - 161.1195681661; + } else { + green = 288.1221695283 * std::pow(t - 60.0, -0.0755148492); + } + + double blue; + if (t >= 66.0) { + blue = 255.0; + } else if (t <= 19.0) { + blue = 0.0; + } else { + blue = 138.5177312231 * std::log(t - 10.0) - 305.0447927307; + } + + // Normalize to the green channel so temperature shifts do not change + // exposure, then let tint move along the green-magenta axis + red /= green; + blue /= green; + green = 1.0; + + const double tint_gain = std::max(0.0, std::min(1.0 + tint, 2.0)); + + return Vector3D(float(red), float(green * tint_gain), float(blue)); +} + +} diff --git a/src/node/src/color/whitebalance/whitebalance.h b/src/node/src/color/whitebalance/whitebalance.h new file mode 100644 index 000000000..498f4f91b --- /dev/null +++ b/src/node/src/color/whitebalance/whitebalance.h @@ -0,0 +1,72 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_WHITEBALANCENODE_H +#define OAK_WHITEBALANCENODE_H + +#include "mathtypes.h" +#include "node.h" + +namespace olive +{ + +/** + * @brief White balance correction by color temperature and tint + * + * Converts a scene illuminant temperature (in Kelvin) into per-channel RGB + * gains using the Tanner Helland blackbody approximation, normalized so the + * green channel is preserved (no exposure shift). Tint shifts the image + * along the green-magenta axis. + */ +class WhiteBalanceNode : public Node { +public: + WhiteBalanceNode(); + + NODE_DEFAULT_FUNCTIONS(WhiteBalanceNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode get_shader_code(const ShaderRequest &request) const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + /** + * @brief RGB gains for a given illuminant temperature and tint + * + * Extracted for testability. Kelvin is clamped to [1000, 40000]; the + * result is normalized so the green channel gain is 1.0 at tint 0. + */ + static Vector3D get_gain_for_temperature(double kelvin, double tint); + + static const std::string k_texture_input; + static const std::string k_temperature_input; + static const std::string k_tint_input; + static const std::string k_gain_input; +}; + +} // olive + +#endif diff --git a/src/node/src/distort/CMakeLists.txt b/src/node/src/distort/CMakeLists.txt new file mode 100644 index 000000000..f71d65b8d --- /dev/null +++ b/src/node/src/distort/CMakeLists.txt @@ -0,0 +1,30 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(cornerpin) +add_subdirectory(crop) +add_subdirectory(flip) +add_subdirectory(mask) +add_subdirectory(ripple) +add_subdirectory(swirl) +add_subdirectory(tile) +add_subdirectory(transform) +add_subdirectory(wave) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/distort/cornerpin/CMakeLists.txt b/src/node/src/distort/cornerpin/CMakeLists.txt new file mode 100644 index 000000000..103b2f427 --- /dev/null +++ b/src/node/src/distort/cornerpin/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/cornerpin/cornerpindistortnode.cpp + node/distort/cornerpin/cornerpindistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/cornerpin/cornerpindistortnode.cpp b/src/node/src/distort/cornerpin/cornerpindistortnode.cpp new file mode 100644 index 000000000..2a3d69f5a --- /dev/null +++ b/src/node/src/distort/cornerpin/cornerpindistortnode.cpp @@ -0,0 +1,223 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "cornerpindistortnode.h" + +#include + +#include "common/lerp.h" + +namespace olive +{ + +const std::string CornerPinDistortNode::k_texture_input = "tex_in"; +const std::string CornerPinDistortNode::k_top_left_input = "top_left_in"; +const std::string CornerPinDistortNode::k_top_right_input = "top_right_in"; +const std::string CornerPinDistortNode::k_bottom_right_input = + "bottom_right_in"; +const std::string CornerPinDistortNode::k_bottom_left_input = "bottom_left_in"; +const std::string CornerPinDistortNode::k_perspective_input = "perspective_in"; + +#define super Node + +CornerPinDistortNode::CornerPinDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + add_input(k_perspective_input, NodeValue::k_boolean, true); + add_input(k_top_left_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); + add_input(k_top_right_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); + add_input(k_bottom_right_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); + add_input(k_bottom_left_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); + + // Initiate gizmos + gizmo_whole_rect_ = add_draggable_gizmo(); + gizmo_resize_handle_[0] = add_draggable_gizmo( + { NodeKeyframeTrackReference(NodeInput(this, k_top_left_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_top_left_input), 1) }); + gizmo_resize_handle_[1] = add_draggable_gizmo( + { NodeKeyframeTrackReference(NodeInput(this, k_top_right_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_top_right_input), 1) }); + gizmo_resize_handle_[2] = add_draggable_gizmo( + { NodeKeyframeTrackReference(NodeInput(this, k_bottom_right_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_bottom_right_input), 1) }); + gizmo_resize_handle_[3] = add_draggable_gizmo( + { NodeKeyframeTrackReference(NodeInput(this, k_bottom_left_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_bottom_left_input), 1) }); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +void CornerPinDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Texture"); + set_input_name(k_perspective_input, "Perspective"); + set_input_name(k_top_left_input, "Top Left"); + set_input_name(k_top_right_input, "Top Right"); + set_input_name(k_bottom_right_input, "Bottom Right"); + set_input_name(k_bottom_left_input, "Bottom Left"); +} + +void CornerPinDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If no texture do nothing + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + // In the special case that all sliders are in their default position just + // push the texture. + if (!(value.at(k_top_left_input).to_vec2().is_null() && + value.at(k_top_right_input).to_vec2().is_null() && + value.at(k_bottom_right_input).to_vec2().is_null() && + value.at(k_bottom_left_input).to_vec2().is_null())) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + + // Convert slider values to their pixel values and then convert to clip space (-1.0 ... 1.0) for overriding the + // vertex coordinates. + const Vector2D &resolution = tex->virtual_resolution(); + Vector2D half_resolution = resolution * 0.5; + PointF top_left_pt = value_to_pixel(0, value, resolution); + Vector2D top_left = Vector2D(top_left_pt.x(), top_left_pt.y()) / + half_resolution - + Vector2D(1.0, 1.0); + PointF top_right_pt = value_to_pixel(1, value, resolution); + Vector2D top_right = + Vector2D(top_right_pt.x(), top_right_pt.y()) / + half_resolution - + Vector2D(1.0, 1.0); + PointF bottom_right_pt = value_to_pixel(2, value, resolution); + Vector2D bottom_right = + Vector2D(bottom_right_pt.x(), bottom_right_pt.y()) / + half_resolution - + Vector2D(1.0, 1.0); + PointF bottom_left_pt = value_to_pixel(3, value, resolution); + Vector2D bottom_left = + Vector2D(bottom_left_pt.x(), bottom_left_pt.y()) / + half_resolution - + Vector2D(1.0, 1.0); + + // Override default vertex coordinates. + std::vector adjusted_vertices = { + top_left.x(), top_left.y(), 0.0f, + top_right.x(), top_right.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f, + + top_left.x(), top_left.y(), 0.0f, + bottom_left.x(), bottom_left.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f + }; + job.set_vertex_coordinates(adjusted_vertices); + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + table->push(value.at(k_texture_input)); + } + } +} + +ShaderCode +CornerPinDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode(FileFunctions::read_file_as_string( + ":/shaders/cornerpin.frag"), + FileFunctions::read_file_as_string( + ":/shaders/cornerpin.vert")); +} + +PointF CornerPinDistortNode::value_to_pixel(int value, const NodeValueRow &row, + const Vector2D &resolution) const +{ + assert(value >= 0 && value <= 3); + + Vector2D v; + + switch (value) { + case 0: // Top left + v = row.at(k_top_left_input).to_vec2(); + return PointF(v.x(), v.y()); + case 1: // Top right + v = row.at(k_top_right_input).to_vec2(); + return PointF(resolution.x() + v.x(), v.y()); + case 2: // Bottom right + v = row.at(k_bottom_right_input).to_vec2(); + return PointF(resolution.x() + v.x(), resolution.y() + v.y()); + case 3: //Bottom left + v = row.at(k_bottom_left_input).to_vec2(); + return PointF(v.x(), v.y() + resolution.y()); + default: // We should never get here + return PointF(); + } +} + +void CornerPinDistortNode::gizmo_drag_move(double x, double y, int modifiers) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + if (gizmo != gizmo_whole_rect_) { + gizmo->get_draggers()[0].drag( + gizmo->get_draggers()[0].get_start_value().to_double() + x); + gizmo->get_draggers()[1].drag( + gizmo->get_draggers()[1].get_start_value().to_double() + y); + } +} + +void CornerPinDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + if (TexturePtr tex = row.at(k_texture_input).to_texture()) { + const Vector2D &resolution = tex->virtual_resolution(); + + PointF top_left = value_to_pixel(0, row, resolution); + PointF top_right = value_to_pixel(1, row, resolution); + PointF bottom_right = value_to_pixel(2, row, resolution); + PointF bottom_left = value_to_pixel(3, row, resolution); + + // Add the correct offset to each slider + set_input_property(k_top_left_input, "offset", + Vector2D(0.0, 0.0)); + set_input_property(k_top_right_input, "offset", + Vector2D(resolution.x(), 0.0)); + set_input_property(k_bottom_right_input, "offset", + resolution); + set_input_property(k_bottom_left_input, "offset", + Vector2D(0.0, resolution.y())); + + // Draw bounding box + gizmo_whole_rect_->set_polygon( + { top_left, top_right, bottom_right, bottom_left, top_left }); + + // Create handles + gizmo_resize_handle_[0]->set_point(top_left); + gizmo_resize_handle_[1]->set_point(top_right); + gizmo_resize_handle_[2]->set_point(bottom_right); + gizmo_resize_handle_[3]->set_point(bottom_left); + } +} + +} diff --git a/src/node/src/distort/cornerpin/cornerpindistortnode.h b/src/node/src/distort/cornerpin/cornerpindistortnode.h new file mode 100644 index 000000000..9419327e4 --- /dev/null +++ b/src/node/src/distort/cornerpin/cornerpindistortnode.h @@ -0,0 +1,95 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_CORNERPINDISTORTNODE_H +#define OAK_CORNERPINDISTORTNODE_H + +#include "gizmo/point.h" +#include "gizmo/polygon.h" +#include "inputdragger.h" +#include "node.h" + +namespace olive +{ +class CornerPinDistortNode : public Node { +public: + CornerPinDistortNode(); + + NODE_DEFAULT_FUNCTIONS(CornerPinDistortNode) + + virtual std::string name() const override + { + return "Corner Pin"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.cornerpin"; + } + + virtual std::vector category() const override + { + return { k_category_distort }; + } + + virtual std::string description() const override + { + return "Distort the image by dragging the corners."; + } + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + /** + * @brief Convenience function - converts the 2D slider values from being + * an offset to the actual pixel value. + */ + PointF value_to_pixel(int value, const NodeValueRow &row, + const Vector2D &resolution) const; + + static const std::string k_texture_input; + static const std::string k_perspective_input; + static const std::string k_top_left_input; + static const std::string k_top_right_input; + static const std::string k_bottom_right_input; + static const std::string k_bottom_left_input; + +protected: + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + // Gizmo variables + static const int k_gizmo_corner_count = 4; + PointGizmo *gizmo_resize_handle_[k_gizmo_corner_count]; + PolygonGizmo *gizmo_whole_rect_; +}; + +} + +#endif // OAK_CORNERPINDISTORTNODE_H diff --git a/src/node/src/distort/crop/CMakeLists.txt b/src/node/src/distort/crop/CMakeLists.txt new file mode 100644 index 000000000..ad1ff7335 --- /dev/null +++ b/src/node/src/distort/crop/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/crop/cropdistortnode.cpp + node/distort/crop/cropdistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/crop/cropdistortnode.cpp b/src/node/src/distort/crop/cropdistortnode.cpp new file mode 100644 index 000000000..25e357a77 --- /dev/null +++ b/src/node/src/distort/crop/cropdistortnode.cpp @@ -0,0 +1,191 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "cropdistortnode.h" + +#include "common/util.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string CropDistortNode::k_texture_input = "tex_in"; +const std::string CropDistortNode::k_left_input = "left_in"; +const std::string CropDistortNode::k_top_input = "top_in"; +const std::string CropDistortNode::k_right_input = "right_in"; +const std::string CropDistortNode::k_bottom_input = "bottom_in"; +const std::string CropDistortNode::k_feather_input = "feather_in"; + +#define super Node + +CropDistortNode::CropDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + create_crop_side_input(k_left_input); + create_crop_side_input(k_top_input); + create_crop_side_input(k_right_input); + create_crop_side_input(k_bottom_input); + + add_input(k_feather_input, NodeValue::k_float, 0.0); + set_input_property(k_feather_input, "min", 0.0); + + // Initiate gizmos + poly_gizmo_ = add_draggable_gizmo( + { k_left_input, k_top_input, k_right_input, k_bottom_input }); + + point_gizmo_[k_gizmo_scale_top_left] = + add_draggable_gizmo({ k_left_input, k_top_input }); + point_gizmo_[k_gizmo_scale_top_center] = + add_draggable_gizmo({ k_top_input }); + point_gizmo_[k_gizmo_scale_top_right] = + add_draggable_gizmo({ k_right_input, k_top_input }); + point_gizmo_[k_gizmo_scale_bottom_left] = + add_draggable_gizmo({ k_left_input, k_bottom_input }); + point_gizmo_[k_gizmo_scale_bottom_center] = + add_draggable_gizmo({ k_bottom_input }); + point_gizmo_[k_gizmo_scale_bottom_right] = + add_draggable_gizmo({ k_right_input, k_bottom_input }); + point_gizmo_[k_gizmo_scale_center_left] = + add_draggable_gizmo({ k_left_input }); + point_gizmo_[k_gizmo_scale_center_right] = + add_draggable_gizmo({ k_right_input }); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +void CropDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Texture"); + set_input_name(k_left_input, "Left"); + set_input_name(k_top_input, "Top"); + set_input_name(k_right_input, "Right"); + set_input_name(k_bottom_input, "Bottom"); + set_input_name(k_feather_input, "Feather"); +} + +void CropDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + ShaderJob job; + job.insert(value); + + if (TexturePtr texture = job.get(k_texture_input).to_texture()) { + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, + Vector2D(texture->params().width(), + texture->params().height()), + this)); + + if (job.get(k_left_input).to_double() != 0.0 || + job.get(k_right_input).to_double() != 0.0 || + job.get(k_top_input).to_double() != 0.0 || + job.get(k_bottom_input).to_double() != 0.0) { + table->push(NodeValue::k_texture, texture->to_job(job), this); + } else { + table->push(job.get(k_texture_input)); + } + } +} + +ShaderCode CropDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/crop.frag")); +} + +void CropDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + if (TexturePtr tex = row.at(k_texture_input).to_texture()) { + const Vector2D &resolution = tex->virtual_resolution(); + temp_resolution_ = resolution; + + double left_pt = resolution.x() * row.at(k_left_input).to_double(); + double top_pt = resolution.y() * row.at(k_top_input).to_double(); + double right_pt = resolution.x() * (1.0 - row.at(k_right_input).to_double()); + double bottom_pt = + resolution.y() * (1.0 - row.at(k_bottom_input).to_double()); + double center_x_pt = mid(left_pt, right_pt); + double center_y_pt = mid(top_pt, bottom_pt); + + point_gizmo_[k_gizmo_scale_top_left]->set_point(PointF(left_pt, top_pt)); + point_gizmo_[k_gizmo_scale_top_center]->set_point( + PointF(center_x_pt, top_pt)); + point_gizmo_[k_gizmo_scale_top_right]->set_point(PointF(right_pt, top_pt)); + point_gizmo_[k_gizmo_scale_bottom_left]->set_point( + PointF(left_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_bottom_center]->set_point( + PointF(center_x_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_bottom_right]->set_point( + PointF(right_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_center_left]->set_point( + PointF(left_pt, center_y_pt)); + point_gizmo_[k_gizmo_scale_center_right]->set_point( + PointF(right_pt, center_y_pt)); + + poly_gizmo_->set_polygon({ PointF(left_pt, top_pt), + PointF(right_pt, top_pt), + PointF(right_pt, bottom_pt), + PointF(left_pt, bottom_pt), + PointF(left_pt, top_pt) }); + } +} + +void CropDistortNode::gizmo_drag_move(double x_diff, double y_diff, + int modifiers) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + Vector2D res = temp_resolution_; + x_diff /= res.x(); + y_diff /= res.y(); + + for (int j = 0; j < int(gizmo->get_draggers().size()); j++) { + NodeInputDragger &i = gizmo->get_draggers()[j]; + double s = i.get_start_value().to_double(); + if (i.get_input().input().input() == k_left_input) { + i.drag(s + x_diff); + } else if (i.get_input().input().input() == k_top_input) { + i.drag(s + y_diff); + } else if (i.get_input().input().input() == k_right_input) { + i.drag(s - x_diff); + } else if (i.get_input().input().input() == k_bottom_input) { + i.drag(s - y_diff); + } + } +} + +void CropDistortNode::create_crop_side_input(const std::string &id) +{ + add_input(id, NodeValue::k_float, 0.0); + set_input_property(id, "min", 0.0); + set_input_property(id, "max", 1.0); + set_input_property(id, "view", slider::k_percentage); +} + +} diff --git a/src/node/src/distort/crop/cropdistortnode.h b/src/node/src/distort/crop/cropdistortnode.h new file mode 100644 index 000000000..747b433c6 --- /dev/null +++ b/src/node/src/distort/crop/cropdistortnode.h @@ -0,0 +1,92 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_CROPDISTORTNODE_H +#define OAK_CROPDISTORTNODE_H + +#include "gizmo/point.h" +#include "gizmo/polygon.h" +#include "inputdragger.h" +#include "node.h" + +namespace olive +{ + +class CropDistortNode : public Node { +public: + CropDistortNode(); + + NODE_DEFAULT_FUNCTIONS(CropDistortNode) + + virtual std::string name() const override + { + return "Crop"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.crop"; + } + + virtual std::vector category() const override + { + return { k_category_distort }; + } + + virtual std::string description() const override + { + return "Crop the edges of an image."; + } + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + static const std::string k_texture_input; + static const std::string k_left_input; + static const std::string k_top_input; + static const std::string k_right_input; + static const std::string k_bottom_input; + static const std::string k_feather_input; + +protected: + virtual void gizmo_drag_move(double delta_x, double delta_y, + int modifiers) override; + +private: + void create_crop_side_input(const std::string &id); + + // Gizmo variables + PointGizmo *point_gizmo_[k_gizmo_scale_count]; + PolygonGizmo *poly_gizmo_; + Vector2D temp_resolution_; +}; + +} + +#endif // OAK_CROPDISTORTNODE_H diff --git a/src/node/src/distort/flip/CMakeLists.txt b/src/node/src/distort/flip/CMakeLists.txt new file mode 100644 index 000000000..49b77008a --- /dev/null +++ b/src/node/src/distort/flip/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/flip/flipdistortnode.cpp + node/distort/flip/flipdistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/flip/flipdistortnode.cpp b/src/node/src/distort/flip/flipdistortnode.cpp new file mode 100644 index 000000000..cca9b5d6f --- /dev/null +++ b/src/node/src/distort/flip/flipdistortnode.cpp @@ -0,0 +1,99 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "flipdistortnode.h" + +namespace olive +{ + +const std::string FlipDistortNode::k_texture_input = "tex_in"; +const std::string FlipDistortNode::k_horizontal_input = "horiz_in"; +const std::string FlipDistortNode::k_vertical_input = "vert_in"; + +#define super Node + +FlipDistortNode::FlipDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_horizontal_input, NodeValue::k_boolean, false); + + add_input(k_vertical_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +std::string FlipDistortNode::name() const +{ + return "Flip"; +} + +std::string FlipDistortNode::id() const +{ + return "org.olivevideoeditor.Olive.flip"; +} + +std::vector FlipDistortNode::category() const +{ + return { k_category_distort }; +} + +std::string FlipDistortNode::description() const +{ + return "Flips an image horizontally or vertically"; +} + +void FlipDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_horizontal_input, "Horizontal"); + set_input_name(k_vertical_input, "Vertical"); +} + +ShaderCode FlipDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/flip.frag")); +} + +void FlipDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + // Only run shader if at least one of flip or flop are selected + if (value.at(k_horizontal_input).to_bool() || + value.at(k_vertical_input).to_bool()) { + table->push(NodeValue::k_texture, tex->to_job(ShaderJob(value)), + this); + } else { + // If we're not flipping or flopping just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +} diff --git a/src/node/src/distort/flip/flipdistortnode.h b/src/node/src/distort/flip/flipdistortnode.h new file mode 100644 index 000000000..c03213590 --- /dev/null +++ b/src/node/src/distort/flip/flipdistortnode.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FLIPDISTORTNODE_H +#define OAK_FLIPDISTORTNODE_H + +#include "node.h" + +namespace olive +{ + +class FlipDistortNode : public Node { +public: + FlipDistortNode(); + + NODE_DEFAULT_FUNCTIONS(FlipDistortNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_horizontal_input; + static const std::string k_vertical_input; +}; + +} + +#endif // OAK_FLIPDISTORTNODE_H diff --git a/src/node/src/distort/mask/CMakeLists.txt b/src/node/src/distort/mask/CMakeLists.txt new file mode 100644 index 000000000..8d980848d --- /dev/null +++ b/src/node/src/distort/mask/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/mask/mask.cpp + node/distort/mask/mask.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/mask/mask.cpp b/src/node/src/distort/mask/mask.cpp new file mode 100644 index 000000000..18dc2026c --- /dev/null +++ b/src/node/src/distort/mask/mask.cpp @@ -0,0 +1,133 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "mask.h" + +#include "filter/blur/blur.h" + +namespace olive +{ + +#define super PolygonGenerator + +const std::string MaskDistortNode::k_feather_input = "feather_in"; +const std::string MaskDistortNode::k_invert_input = "invert_in"; + +MaskDistortNode::MaskDistortNode() +{ + // Mask should always be (1.0, 1.0, 1.0) for multiply to work correctly + set_input_flag(k_color_input, k_input_flag_hidden); + + add_input(k_invert_input, NodeValue::k_boolean, false); + + add_input(k_feather_input, NodeValue::k_float, 0.0); + set_input_property(k_feather_input, "min", 0.0); +} + +ShaderCode MaskDistortNode::get_shader_code(const ShaderRequest &request) const +{ + if (request.id == "mrg") { + return ShaderCode(FileFunctions::read_file_as_string( + ":/shaders/multiply.frag")); + } else if (request.id == "feather") { + return ShaderCode(FileFunctions::read_file_as_string( + ":/shaders/blur.frag")); + } else if (request.id == "invert") { + return ShaderCode(FileFunctions::read_file_as_string( + ":/shaders/invertrgba.frag")); + } else { + return super::get_shader_code(request); + } +} + +void MaskDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_base_input, "Texture"); + set_input_name(k_invert_input, "Invert"); + set_input_name(k_feather_input, "Feather"); +} + +void MaskDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + TexturePtr texture = value.at(k_base_input).to_texture(); + + VideoParams job_params = texture ? texture->params() : globals.vparams(); + NodeValue job(NodeValue::k_texture, + Texture::job(job_params, get_generate_job(value, job_params)), + this); + + if (value.at(k_invert_input).to_bool()) { + ShaderJob invert; + invert.set_shader_id("invert"); + invert.insert("tex_in", job); + job.set_value(Texture::job(job_params, invert)); + } + + if (texture) { + // Push as merge node + ShaderJob merge; + + merge.set_shader_id("mrg"); + merge.insert("tex_a", value.at(k_base_input)); + + if (value.at(k_feather_input).to_double() > 0.0) { + // Nest a blur shader in there too + ShaderJob feather; + + feather.set_shader_id("feather"); + feather.insert(BlurFilterNode::k_texture_input, job); + feather.insert(BlurFilterNode::k_method_input, + NodeValue(NodeValue::k_int, + int(BlurFilterNode::k_gaussian), this)); + feather.insert(BlurFilterNode::k_horiz_input, + NodeValue(NodeValue::k_boolean, true, this)); + feather.insert(BlurFilterNode::k_vert_input, + NodeValue(NodeValue::k_boolean, true, this)); + feather.insert(BlurFilterNode::k_repeat_edge_pixels_input, + NodeValue(NodeValue::k_boolean, true, this)); + feather.insert(BlurFilterNode::k_radius_input, + NodeValue(NodeValue::k_float, + value.at(k_feather_input).to_double(), this)); + feather.set_iterations(2, BlurFilterNode::k_texture_input); + feather.insert("resolution_in", + NodeValue(NodeValue::k_vec2, + texture ? texture->virtual_resolution() : + globals.square_resolution(), + this)); + + merge.insert("tex_b", + NodeValue(NodeValue::k_texture, + Texture::job(job_params, feather), this)); + } else { + merge.insert("tex_b", job); + } + + table->push(NodeValue::k_texture, Texture::job(job_params, merge), this); + } else { + table->push(job); + } +} + +} diff --git a/src/node/src/distort/mask/mask.h b/src/node/src/distort/mask/mask.h new file mode 100644 index 000000000..8bc79ddf3 --- /dev/null +++ b/src/node/src/distort/mask/mask.h @@ -0,0 +1,70 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MASKDISTORTNODE_H +#define OAK_MASKDISTORTNODE_H + +#include "generator/polygon/polygon.h" + +namespace olive +{ + +class MaskDistortNode : public PolygonGenerator { +public: + MaskDistortNode(); + + NODE_DEFAULT_FUNCTIONS(MaskDistortNode) + + virtual std::string name() const override + { + return "Mask"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.mask"; + } + + virtual std::vector category() const override + { + return { k_category_distort }; + } + + virtual std::string description() const override + { + return "Apply a polygonal mask."; + } + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_invert_input; + static const std::string k_feather_input; +}; + +} + +#endif // OAK_MASKDISTORTNODE_H diff --git a/src/node/src/distort/ripple/CMakeLists.txt b/src/node/src/distort/ripple/CMakeLists.txt new file mode 100644 index 000000000..a8cd1168d --- /dev/null +++ b/src/node/src/distort/ripple/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/ripple/rippledistortnode.cpp + node/distort/ripple/rippledistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/ripple/rippledistortnode.cpp b/src/node/src/distort/ripple/rippledistortnode.cpp new file mode 100644 index 000000000..e94e1dd85 --- /dev/null +++ b/src/node/src/distort/ripple/rippledistortnode.cpp @@ -0,0 +1,137 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "rippledistortnode.h" + +namespace olive +{ + +const std::string RippleDistortNode::k_texture_input = "tex_in"; +const std::string RippleDistortNode::k_evolution_input = "evolution_in"; +const std::string RippleDistortNode::k_intensity_input = "intensity_in"; +const std::string RippleDistortNode::k_frequency_input = "frequency_in"; +const std::string RippleDistortNode::k_position_input = "position_in"; +const std::string RippleDistortNode::k_stretch_input = "stretch_in"; + +#define super Node + +RippleDistortNode::RippleDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_evolution_input, NodeValue::k_float, 0); + add_input(k_intensity_input, NodeValue::k_float, 100); + + add_input(k_frequency_input, NodeValue::k_float, 1); + set_input_property(k_frequency_input, "base", 0.01); + + add_input(k_position_input, NodeValue::k_vec2, Vector2D(0, 0)); + add_input(k_stretch_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); + + gizmo_ = add_draggable_gizmo({ + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1), + }); + gizmo_->set_shape(PointGizmo::k_anchor_point); +} + +std::string RippleDistortNode::name() const +{ + return "Ripple"; +} + +std::string RippleDistortNode::id() const +{ + return "org.olivevideoeditor.Olive.ripple"; +} + +std::vector RippleDistortNode::category() const +{ + return { k_category_distort }; +} + +std::string RippleDistortNode::description() const +{ + return "Distorts an image with a ripple effect."; +} + +void RippleDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_frequency_input, "Frequency"); + set_input_name(k_intensity_input, "Intensity"); + set_input_name(k_evolution_input, "Evolution"); + set_input_name(k_position_input, "Position"); + set_input_name(k_stretch_input, "Stretch"); +} + +ShaderCode RippleDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/ripple.frag")); +} + +void RippleDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + // Only run shader if at least one of flip or flop are selected + if (value.at(k_intensity_input).to_double() != 0.0) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + // If we're not flipping or flopping just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +void RippleDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + if (TexturePtr tex = row.at(k_texture_input).to_texture()) { + PointF half_res(tex->virtual_resolution().x() / 2, + tex->virtual_resolution().y() / 2); + gizmo_->set_point(half_res + row.at(k_position_input).to_vec2().to_point_f()); + } +} + +void RippleDistortNode::gizmo_drag_move(double x, double y, int modifiers) +{ + NodeInputDragger &x_drag = gizmo_->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo_->get_draggers()[1]; + + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); +} + +} diff --git a/src/node/src/distort/ripple/rippledistortnode.h b/src/node/src/distort/ripple/rippledistortnode.h new file mode 100644 index 000000000..8b72987cf --- /dev/null +++ b/src/node/src/distort/ripple/rippledistortnode.h @@ -0,0 +1,68 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_RIPPLEDISTORTNODE_H +#define OAK_RIPPLEDISTORTNODE_H + +#include "gizmo/point.h" +#include "node.h" + +namespace olive +{ + +class RippleDistortNode : public Node { +public: + RippleDistortNode(); + + NODE_DEFAULT_FUNCTIONS(RippleDistortNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + static const std::string k_texture_input; + static const std::string k_evolution_input; + static const std::string k_intensity_input; + static const std::string k_frequency_input; + static const std::string k_position_input; + static const std::string k_stretch_input; + +protected: + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + PointGizmo *gizmo_; +}; + +} + +#endif // OAK_RIPPLEDISTORTNODE_H diff --git a/src/node/src/distort/swirl/CMakeLists.txt b/src/node/src/distort/swirl/CMakeLists.txt new file mode 100644 index 000000000..bb2b20ba5 --- /dev/null +++ b/src/node/src/distort/swirl/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/swirl/swirldistortnode.cpp + node/distort/swirl/swirldistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/swirl/swirldistortnode.cpp b/src/node/src/distort/swirl/swirldistortnode.cpp new file mode 100644 index 000000000..77534b012 --- /dev/null +++ b/src/node/src/distort/swirl/swirldistortnode.cpp @@ -0,0 +1,132 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "swirldistortnode.h" + +namespace olive +{ + +const std::string SwirlDistortNode::k_texture_input = "tex_in"; +const std::string SwirlDistortNode::k_radius_input = "radius_in"; +const std::string SwirlDistortNode::k_angle_input = "angle_in"; +const std::string SwirlDistortNode::k_position_input = "pos_in"; + +#define super Node + +SwirlDistortNode::SwirlDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_radius_input, NodeValue::k_float, 200); + set_input_property(k_radius_input, "min", 0); + + add_input(k_angle_input, NodeValue::k_float, 10); + set_input_property(k_angle_input, "base", 0.1); + + add_input(k_position_input, NodeValue::k_vec2, Vector2D(0, 0)); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); + + gizmo_ = add_draggable_gizmo({ + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1), + }); + gizmo_->set_shape(PointGizmo::k_anchor_point); +} + +std::string SwirlDistortNode::name() const +{ + return "Swirl"; +} + +std::string SwirlDistortNode::id() const +{ + return "org.olivevideoeditor.Olive.swirl"; +} + +std::vector SwirlDistortNode::category() const +{ + return { k_category_distort }; +} + +std::string SwirlDistortNode::description() const +{ + return "Distorts an image by swirling it around a center point."; +} + +void SwirlDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_radius_input, "Radius"); + set_input_name(k_angle_input, "Angle"); + set_input_name(k_position_input, "Position"); +} + +ShaderCode SwirlDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/swirl.frag")); +} + +void SwirlDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + // Only run shader if at least one of flip or flop are selected + if (value.at(k_angle_input).to_double() != 0.0 && + value.at(k_radius_input).to_double() != 0.0) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + // If we're not flipping or flopping just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +void SwirlDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + PointF half_res(globals.square_resolution().x() / 2, + globals.square_resolution().y() / 2); + + gizmo_->set_point(half_res + row.at(k_position_input).to_vec2().to_point_f()); +} + +void SwirlDistortNode::gizmo_drag_move(double x, double y, int modifiers) +{ + NodeInputDragger &x_drag = gizmo_->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo_->get_draggers()[1]; + + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); +} + +} diff --git a/src/node/src/distort/swirl/swirldistortnode.h b/src/node/src/distort/swirl/swirldistortnode.h new file mode 100644 index 000000000..b217b3348 --- /dev/null +++ b/src/node/src/distort/swirl/swirldistortnode.h @@ -0,0 +1,66 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SWIRLDISTORTNODE_H +#define OAK_SWIRLDISTORTNODE_H + +#include "gizmo/point.h" +#include "node.h" + +namespace olive +{ + +class SwirlDistortNode : public Node { +public: + SwirlDistortNode(); + + NODE_DEFAULT_FUNCTIONS(SwirlDistortNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + static const std::string k_texture_input; + static const std::string k_radius_input; + static const std::string k_angle_input; + static const std::string k_position_input; + +protected: + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + PointGizmo *gizmo_; +}; + +} + +#endif // OAK_SWIRLDISTORTNODE_H diff --git a/src/node/src/distort/tile/CMakeLists.txt b/src/node/src/distort/tile/CMakeLists.txt new file mode 100644 index 000000000..97a280baa --- /dev/null +++ b/src/node/src/distort/tile/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/tile/tiledistortnode.cpp + node/distort/tile/tiledistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/tile/tiledistortnode.cpp b/src/node/src/distort/tile/tiledistortnode.cpp new file mode 100644 index 000000000..fd0699dc5 --- /dev/null +++ b/src/node/src/distort/tile/tiledistortnode.cpp @@ -0,0 +1,180 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "tiledistortnode.h" + +#include + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string TileDistortNode::k_texture_input = "tex_in"; +const std::string TileDistortNode::k_scale_input = "scale_in"; +const std::string TileDistortNode::k_position_input = "position_in"; +const std::string TileDistortNode::k_anchor_input = "anchor_in"; +const std::string TileDistortNode::k_mirror_x_input = "mirrorx_in"; +const std::string TileDistortNode::k_mirror_y_input = "mirrory_in"; + +#define super Node + +TileDistortNode::TileDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_scale_input, NodeValue::k_float, 0.5); + set_input_property(k_scale_input, "min", 0); + set_input_property(k_scale_input, "view", + slider::k_percentage); + + add_input(k_position_input, NodeValue::k_vec2, Vector2D(0, 0)); + + add_input(k_anchor_input, NodeValue::k_combo, k_middle_center); + + add_input(k_mirror_x_input, NodeValue::k_boolean, false); + add_input(k_mirror_y_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); + + gizmo_ = add_draggable_gizmo({ + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1), + }); + gizmo_->set_shape(PointGizmo::k_anchor_point); +} + +std::string TileDistortNode::name() const +{ + return "Tile"; +} + +std::string TileDistortNode::id() const +{ + return "org.olivevideoeditor.Olive.tile"; +} + +std::vector TileDistortNode::category() const +{ + return { k_category_distort }; +} + +std::string TileDistortNode::description() const +{ + return "Infinitely tile an image horizontally and vertically."; +} + +void TileDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_scale_input, "Scale"); + set_input_name(k_position_input, "Position"); + set_input_name(k_mirror_x_input, "Mirror Horizontally"); + set_input_name(k_mirror_y_input, "Mirror Vertically"); + + set_input_name(k_anchor_input, "Anchor"); + set_combo_box_strings(k_anchor_input, { + "Top-Left", + "Top-Center", + "Top-Right", + "Middle-Left", + "Middle-Center", + "Middle-Right", + "Bottom-Left", + "Bottom-Center", + "Bottom-Right", + }); +} + +ShaderCode TileDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/tile.frag")); +} + +void TileDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + // Only run shader if at least one of flip or flop are selected + double scale_value = value.at(k_scale_input).to_double(); + if (!(std::abs(scale_value - 1.0) * 1000000000000.0 <= + std::min(std::abs(scale_value), std::abs(1.0)))) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + // If we're not flipping or flopping just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +void TileDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + if (TexturePtr tex = row.at(k_texture_input).to_texture()) { + PointF res = tex->virtual_resolution().to_point_f(); + PointF pos = row.at(k_position_input).to_vec2().to_point_f(); + double x = pos.x(); + double y = pos.y(); + + Anchor a = static_cast(row.at(k_anchor_input).to_int()); + if (a == k_top_left || a == k_top_center || a == k_top_right) { + // Do nothing + } else if (a == k_middle_left || a == k_middle_center || + a == k_middle_right) { + y += res.y() / 2; + } else if (a == k_bottom_left || a == k_bottom_center || + a == k_bottom_right) { + y += res.y(); + } + if (a == k_top_left || a == k_middle_left || a == k_bottom_left) { + // Do nothing + } else if (a == k_top_center || a == k_middle_center || + a == k_bottom_center) { + x += res.x() / 2; + } else if (a == k_top_right || a == k_middle_right || a == k_bottom_right) { + x += res.x(); + } + + gizmo_->set_point(PointF(x, y)); + } +} + +void TileDistortNode::gizmo_drag_move(double x, double y, int modifiers) +{ + NodeInputDragger &x_drag = gizmo_->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo_->get_draggers()[1]; + + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); +} + +} diff --git a/src/node/src/distort/tile/tiledistortnode.h b/src/node/src/distort/tile/tiledistortnode.h new file mode 100644 index 000000000..271ddbe94 --- /dev/null +++ b/src/node/src/distort/tile/tiledistortnode.h @@ -0,0 +1,80 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TILEDISTORTNODE_H +#define OAK_TILEDISTORTNODE_H + +#include "gizmo/point.h" +#include "node.h" + +namespace olive +{ + +class TileDistortNode : public Node { +public: + TileDistortNode(); + + NODE_DEFAULT_FUNCTIONS(TileDistortNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + static const std::string k_texture_input; + static const std::string k_scale_input; + static const std::string k_position_input; + static const std::string k_anchor_input; + static const std::string k_mirror_x_input; + static const std::string k_mirror_y_input; + +protected: + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + enum Anchor { + k_top_left, + k_top_center, + k_top_right, + k_middle_left, + k_middle_center, + k_middle_right, + k_bottom_left, + k_bottom_center, + k_bottom_right + }; + + PointGizmo *gizmo_; +}; + +} + +#endif // OAK_TILEDISTORTNODE_H diff --git a/src/node/src/distort/transform/CMakeLists.txt b/src/node/src/distort/transform/CMakeLists.txt new file mode 100644 index 000000000..584116767 --- /dev/null +++ b/src/node/src/distort/transform/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/transform/transformdistortnode.cpp + node/distort/transform/transformdistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/transform/transformdistortnode.cpp b/src/node/src/distort/transform/transformdistortnode.cpp new file mode 100644 index 000000000..dea8384e5 --- /dev/null +++ b/src/node/src/distort/transform/transformdistortnode.cpp @@ -0,0 +1,513 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "transformdistortnode.h" + +#include + +namespace olive +{ + +const std::string TransformDistortNode::k_parent_input = "parent_in"; +const std::string TransformDistortNode::k_texture_input = "tex_in"; +const std::string TransformDistortNode::k_autoscale_input = "autoscale_in"; +const std::string TransformDistortNode::k_interpolation_input = + "interpolation_in"; + +#define super MatrixGenerator + +TransformDistortNode::TransformDistortNode() +{ + add_input(k_parent_input, NodeValue::k_matrix); + + add_input(k_autoscale_input, NodeValue::k_combo, 0); + + add_input(k_interpolation_input, NodeValue::k_combo, 2); + + prepend_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + // Initiate gizmos + rotation_gizmo_ = add_draggable_gizmo(); + rotation_gizmo_->add_input(NodeInput(this, k_rotation_input)); + rotation_gizmo_->set_drag_value_behavior(ScreenGizmo::k_absolute); + + poly_gizmo_ = add_draggable_gizmo(); + poly_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0)); + poly_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1)); + + anchor_gizmo_ = add_draggable_gizmo(); + anchor_gizmo_->set_shape(PointGizmo::k_anchor_point); + anchor_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_anchor_input), 0)); + anchor_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_anchor_input), 1)); + anchor_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0)); + anchor_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1)); + + for (int i = 0; i < k_gizmo_scale_count; i++) { + point_gizmo_[i] = add_draggable_gizmo(); + point_gizmo_[i]->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_scale_input), 0)); + point_gizmo_[i]->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_scale_input), 1)); + point_gizmo_[i]->set_drag_value_behavior(PointGizmo::k_absolute); + } + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +void TransformDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_parent_input, "Parent"); + set_input_name(k_autoscale_input, "Auto-Scale"); + set_input_name(k_texture_input, "Texture"); + set_input_name(k_interpolation_input, "Interpolation"); + + set_combo_box_strings(k_autoscale_input, + { "None", "Fit", "Fill", "Stretch" }); + set_combo_box_strings(k_interpolation_input, + { "Nearest Neighbor", "Bilinear", + "Mipmapped Bilinear" }); +} + +void TransformDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Generate matrix + Matrix4x4 generated_matrix = generate_matrix( + value, false, false, false, value.at(k_parent_input).to_matrix()); + + // Pop texture + NodeValue texture_meta = value.at(k_texture_input); + + TexturePtr job_to_push = nullptr; + + // If we have a texture, generate a matrix and make it happen + if (TexturePtr texture = texture_meta.to_texture()) { + // Adjust our matrix by the resolutions involved + Matrix4x4 real_matrix = generate_auto_scaled_matrix( + generated_matrix, value, globals, texture->params()); + + if (!real_matrix.is_identity()) { + // The matrix will transform things + ShaderJob job; + job.insert("ove_maintex", texture_meta); + job.insert("ove_mvpmat", + NodeValue(NodeValue::k_matrix, real_matrix, this)); + job.set_interpolation("ove_maintex", + static_cast( + value.at(k_interpolation_input).to_int())); + + // Use global resolution rather than texture resolution because this may result in a size change + job_to_push = Texture::job(globals.vparams(), job); + } + } + + table->push(NodeValue::k_matrix, Variant::from_value(generated_matrix), + this); + + if (!job_to_push) { + // Re-push whatever value we received + table->push(texture_meta); + } else { + table->push(NodeValue::k_texture, job_to_push, this); + } +} + +ShaderCode +TransformDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + // Returns default frag and vert shader + return ShaderCode(); +} + +void TransformDistortNode::gizmo_drag_start(const NodeValueRow &row, double x, + double y, const Rational &time) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + if (gizmo == anchor_gizmo_) { + gizmo_inverted_transform_ = + generate_matrix(row, true, true, false, row.at(k_parent_input).to_matrix()) + .inverted(); + + } else if (is_a_scale_gizmo(gizmo)) { + // Dragging scale handle + TexturePtr tex = row.at(k_texture_input).to_texture(); + if (!tex) { + return; + } + + gizmo_scale_uniform_ = row.at(k_uniform_scale_input).to_bool(); + gizmo_anchor_pt_ = (row.at(k_anchor_input).to_vec2() + + gizmo->get_globals().nonsquare_resolution() / 2) + .to_point_f(); + + if (gizmo == point_gizmo_[k_gizmo_scale_top_left] || + gizmo == point_gizmo_[k_gizmo_scale_top_right] || + gizmo == point_gizmo_[k_gizmo_scale_bottom_left] || + gizmo == point_gizmo_[k_gizmo_scale_bottom_right]) { + gizmo_scale_axes_ = k_gizmo_scale_both; + } else if (gizmo == point_gizmo_[k_gizmo_scale_center_left] || + gizmo == point_gizmo_[k_gizmo_scale_center_right]) { + gizmo_scale_axes_ = k_gizmo_scale_x_only; + } else { + gizmo_scale_axes_ = k_gizmo_scale_y_only; + } + + // Store texture size + VideoParams texture_params = tex->params(); + Vector2D texture_sz(texture_params.square_pixel_width(), + texture_params.height()); + gizmo_scale_anchor_ = row.at(k_anchor_input).to_vec2() + texture_sz / 2; + + if (gizmo == point_gizmo_[k_gizmo_scale_top_right] || + gizmo == point_gizmo_[k_gizmo_scale_bottom_right] || + gizmo == point_gizmo_[k_gizmo_scale_center_right]) { + // Right handles, flip X axis + gizmo_scale_anchor_.set_x(texture_sz.x() - gizmo_scale_anchor_.x()); + } + + if (gizmo == point_gizmo_[k_gizmo_scale_bottom_left] || + gizmo == point_gizmo_[k_gizmo_scale_bottom_right] || + gizmo == point_gizmo_[k_gizmo_scale_bottom_center]) { + // Bottom handles, flip Y axis + gizmo_scale_anchor_.set_y(texture_sz.y() - gizmo_scale_anchor_.y()); + } + + // Store current matrix + gizmo_inverted_transform_ = + generate_matrix(row, true, true, true, row.at(k_parent_input).to_matrix()) + .inverted(); + + } else if (gizmo == rotation_gizmo_) { + gizmo_anchor_pt_ = (row.at(k_anchor_input).to_vec2() + + gizmo->get_globals().nonsquare_resolution() / 2) + .to_point_f(); + gizmo_start_angle_ = + std::atan2(y - gizmo_anchor_pt_.y(), x - gizmo_anchor_pt_.x()); + gizmo_last_angle_ = gizmo_start_angle_; + gizmo_last_alt_angle_ = + std::atan2(x - gizmo_anchor_pt_.x(), y - gizmo_anchor_pt_.y()); + gizmo_rotate_wrap_ = 0; + gizmo_rotate_last_dir_ = k_direction_none; + } +} + +void TransformDistortNode::gizmo_drag_move(double x, double y, int modifiers) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + if (gizmo == poly_gizmo_) { + NodeInputDragger &x_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo->get_draggers()[1]; + + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); + + } else if (gizmo == anchor_gizmo_) { + NodeInputDragger &x_anchor_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_anchor_drag = gizmo->get_draggers()[1]; + NodeInputDragger &x_pos_drag = gizmo->get_draggers()[2]; + NodeInputDragger &y_pos_drag = gizmo->get_draggers()[3]; + + PointF inverted_movement(gizmo_inverted_transform_.map(PointF(x, y))); + + x_anchor_drag.drag(x_anchor_drag.get_start_value().to_double() + + inverted_movement.x()); + y_anchor_drag.drag(y_anchor_drag.get_start_value().to_double() + + inverted_movement.y()); + x_pos_drag.drag(x_pos_drag.get_start_value().to_double() + x); + y_pos_drag.drag(y_pos_drag.get_start_value().to_double() + y); + + } else if (gizmo == rotation_gizmo_) { + double raw_angle = + std::atan2(y - gizmo_anchor_pt_.y(), x - gizmo_anchor_pt_.x()); + double alt_angle = + std::atan2(x - gizmo_anchor_pt_.x(), y - gizmo_anchor_pt_.y()); + + double current_angle = raw_angle; + + // Detect rotation wrap around + RotationDirection this_dir = + get_direction_from_angles(gizmo_last_angle_, raw_angle); + RotationDirection alt_dir = + get_direction_from_angles(gizmo_last_alt_angle_, alt_angle); + + if (gizmo_rotate_last_dir_ != k_direction_none && + this_dir != gizmo_rotate_last_dir_) { + if (alt_dir == gizmo_rotate_last_alt_dir_) { + if ((raw_angle - gizmo_last_angle_) < 0) { + gizmo_rotate_wrap_++; + } else { + gizmo_rotate_wrap_--; + } + + this_dir = gizmo_rotate_last_dir_; + alt_dir = gizmo_rotate_last_alt_dir_; + } + } + + gizmo_rotate_last_dir_ = this_dir; + gizmo_rotate_last_alt_dir_ = alt_dir; + gizmo_last_angle_ = raw_angle; + gizmo_last_alt_angle_ = alt_angle; + + current_angle += M_PI * 2 * gizmo_rotate_wrap_; + + // Convert radians to degrees + double rotation_difference = + (current_angle - gizmo_start_angle_) * 57.2958; + + NodeInputDragger &d = gizmo->get_draggers()[0]; + d.drag(d.get_start_value().to_double() + rotation_difference); + + } else if (is_a_scale_gizmo(gizmo)) { + PointF mouse_relative = + gizmo_inverted_transform_.map(PointF(x, y) - gizmo_anchor_pt_); + + double x_scaled_movement = + std::abs(mouse_relative.x() / gizmo_scale_anchor_.x()); + double y_scaled_movement = + std::abs(mouse_relative.y() / gizmo_scale_anchor_.y()); + + NodeInputDragger &x_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo->get_draggers()[1]; + + switch (gizmo_scale_axes_) { + case k_gizmo_scale_x_only: + x_drag.drag(x_scaled_movement); + break; + case k_gizmo_scale_y_only: + if (gizmo_scale_uniform_) { + x_drag.drag(y_scaled_movement); + } else { + y_drag.drag(y_scaled_movement); + } + break; + case k_gizmo_scale_both: + if (gizmo_scale_uniform_) { + double distance = + std::hypot(mouse_relative.x(), mouse_relative.y()); + double texture_diag = std::hypot(gizmo_scale_anchor_.x(), + gizmo_scale_anchor_.y()); + + x_drag.drag(std::abs(distance / texture_diag)); + } else { + x_drag.drag(x_scaled_movement); + y_drag.drag(y_scaled_movement); + } + break; + } + } +} + +Matrix4x4 TransformDistortNode::adjust_matrix_by_resolutions( + const Matrix4x4 &mat, const Vector2D &sequence_res, + const Vector2D &texture_res, const Vector2D &offset, + AutoScaleType autoscale_type) +{ + // First, create an identity matrix + Matrix4x4 adjusted_matrix; + + // Scale it to a square based on the sequence's resolution + adjusted_matrix.scale(2.0 / sequence_res.x(), 2.0 / sequence_res.y(), 1.0); + + // Apply offset if applicable + adjusted_matrix.translate(offset.x(), offset.y()); + + // Adjust by the matrix we generated earlier + adjusted_matrix *= mat; + + // Scale back out to texture size (adjusted by pixel aspect) + adjusted_matrix.scale(texture_res.x() * 0.5, texture_res.y() * 0.5, 1.0); + + // If auto-scale is enabled, fit the texture to the sequence (without cropping) + if (autoscale_type != k_auto_scale_none) { + if (autoscale_type == k_auto_scale_stretch) { + adjusted_matrix.scale(sequence_res.x() / texture_res.x(), + sequence_res.y() / texture_res.y(), 1.0); + } else { + double footage_real_ar = texture_res.x() / texture_res.y(); + double sequence_real_ar = sequence_res.x() / sequence_res.y(); + + double scale_by_x = sequence_res.x() / texture_res.x(); + double scale_by_y = sequence_res.y() / texture_res.y(); + double autoscale_val; + + if ((autoscale_type == k_auto_scale_fit) == + (sequence_real_ar > footage_real_ar)) { + // Scale by height. Either the sequence is wider than the footage or we're using fill and + // cutting off the sides + autoscale_val = scale_by_y; + } else { + // Scale by width. Either the footage is wider than the sequence or we're using fill and + // cutting off the top and bottom + autoscale_val = scale_by_x; + } + + adjusted_matrix.scale(autoscale_val, autoscale_val, 1.0); + } + } + + return adjusted_matrix; +} + +void TransformDistortNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + TexturePtr tex = row.at(k_texture_input).to_texture(); + if (!tex) { + return; + } + + // Get the sequence resolution + const Vector2D &sequence_res = globals.nonsquare_resolution(); + Vector2D sequence_half_res = sequence_res * 0.5; + PointF sequence_half_res_pt = sequence_half_res.to_point_f(); + + // GizmoTraverser just returns the sizes of the textures and no other data + VideoParams tex_params = tex->params(); + Vector2D tex_sz(tex_params.square_pixel_width(), tex_params.height()); + Vector2D tex_offset(tex_params.x(), tex_params.y()); + + // Retrieve autoscale value + AutoScaleType autoscale = + static_cast(row.at(k_autoscale_input).to_int()); + + // Fold values into a matrix for the rectangle + Matrix4x4 rectangle_matrix; + rectangle_matrix.scale(sequence_half_res.x(), sequence_half_res.y()); + rectangle_matrix *= adjust_matrix_by_resolutions( + generate_matrix(row, false, false, false, row.at(k_parent_input).to_matrix()), + sequence_res, tex_sz, tex_offset, autoscale); + + // Create rect and transform it + const std::vector points = { PointF(-1, -1), PointF(1, -1), + PointF(1, 1), PointF(-1, 1), + PointF(-1, -1) }; + std::vector r; + r.reserve(points.size()); + for (const PointF &p : points) { + r.push_back(rectangle_matrix.map(p)); + } + for (PointF &p : r) { + p += sequence_half_res_pt; + } + poly_gizmo_->set_polygon(r); + + // Draw anchor point + Matrix4x4 anchor_matrix; + anchor_matrix.scale(sequence_half_res.x(), sequence_half_res.y()); + anchor_matrix *= adjust_matrix_by_resolutions( + generate_matrix(row, true, false, false, row.at(k_parent_input).to_matrix()), + sequence_res, tex_sz, tex_offset, autoscale); + anchor_gizmo_->set_point(anchor_matrix.map(PointF(0, 0)) + + sequence_half_res_pt); + + // Draw scale handles + point_gizmo_[k_gizmo_scale_top_left]->set_point( + create_scale_point(-1, -1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_top_center]->set_point( + create_scale_point(0, -1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_top_right]->set_point( + create_scale_point(1, -1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_bottom_left]->set_point( + create_scale_point(-1, 1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_bottom_center]->set_point( + create_scale_point(0, 1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_bottom_right]->set_point( + create_scale_point(1, 1, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_center_left]->set_point( + create_scale_point(-1, 0, sequence_half_res_pt, rectangle_matrix)); + point_gizmo_[k_gizmo_scale_center_right]->set_point( + create_scale_point(1, 0, sequence_half_res_pt, rectangle_matrix)); + + // Use offsets to make the appearance of values that start in the top left, even though we + // really anchor around the center + set_input_property(k_position_input, "offset", + sequence_half_res + tex_offset); + set_input_property(k_anchor_input, "offset", tex_sz * 0.5); +} + +Matrix4x4 +TransformDistortNode::gizmo_transformation(const NodeValueRow &row, + const NodeGlobals &globals) const +{ + if (TexturePtr texture = row.at(k_texture_input).to_texture()) { + //auto m = GenerateMatrix(row, false, false, false, row[kParentInput].toMatrix()); + auto m = generate_matrix(row, false, false, false, Matrix4x4()); + return generate_auto_scaled_matrix(m, row, globals, texture->params()); + } + return super::gizmo_transformation(row, globals); +} + +PointF TransformDistortNode::create_scale_point(double x, double y, + const PointF &half_res, + const Matrix4x4 &mat) +{ + return mat.map(PointF(x, y)) + half_res; +} + +Matrix4x4 TransformDistortNode::generate_auto_scaled_matrix( + const Matrix4x4 &generated_matrix, const NodeValueRow &value, + const NodeGlobals &globals, const VideoParams &texture_params) const +{ + const Vector2D &sequence_res = globals.nonsquare_resolution(); + Vector2D texture_res(texture_params.square_pixel_width(), + texture_params.height()); + AutoScaleType autoscale = + static_cast(value.at(k_autoscale_input).to_int()); + + return adjust_matrix_by_resolutions(generated_matrix, sequence_res, + texture_res, Vector2D(texture_params.x(), texture_params.y()), + autoscale); +} + +bool TransformDistortNode::is_a_scale_gizmo(NodeGizmo *g) const +{ + for (int i = 0; i < k_gizmo_scale_count; i++) { + if (point_gizmo_[i] == g) { + return true; + } + } + + return false; +} + +TransformDistortNode::RotationDirection +TransformDistortNode::get_direction_from_angles(double last, double current) +{ + return (current > last) ? k_direction_positive : k_direction_negative; +} + +} diff --git a/src/node/src/distort/transform/transformdistortnode.h b/src/node/src/distort/transform/transformdistortnode.h new file mode 100644 index 000000000..fc3c240bd --- /dev/null +++ b/src/node/src/distort/transform/transformdistortnode.h @@ -0,0 +1,147 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TRANSFORMDISTORTNODE_H +#define OAK_TRANSFORMDISTORTNODE_H + +#include "generator/matrix/matrix.h" +#include "gizmo/point.h" +#include "gizmo/polygon.h" +#include "gizmo/screen.h" + +namespace olive +{ + +class TransformDistortNode : public MatrixGenerator { +public: + TransformDistortNode(); + + NODE_DEFAULT_FUNCTIONS(TransformDistortNode) + + virtual std::string name() const override + { + return "Transform"; + } + + virtual std::string short_name() const override + { + // Override MatrixGenerator's short name "Ortho" + return name(); + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.transform"; + } + + virtual std::vector category() const override + { + return { k_category_distort }; + } + + virtual std::string description() const override + { + return "Transform an image in 2D space. Equivalent to multiplying by an orthographic matrix."; + } + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + enum AutoScaleType { + k_auto_scale_none, + k_auto_scale_fit, + k_auto_scale_fill, + k_auto_scale_stretch + }; + + static Matrix4x4 adjust_matrix_by_resolutions( + const Matrix4x4 &mat, const Vector2D &sequence_res, + const Vector2D &texture_res, const Vector2D &offset, + AutoScaleType autoscale_type = k_auto_scale_none); + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + virtual Matrix4x4 + gizmo_transformation(const NodeValueRow &row, + const NodeGlobals &globals) const override; + + static const std::string k_parent_input; + static const std::string k_texture_input; + static const std::string k_autoscale_input; + static const std::string k_interpolation_input; + +protected: + virtual void gizmo_drag_start(const olive::NodeValueRow &row, double x, + double y, const olive::Rational &time) override; + + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + static PointF create_scale_point(double x, double y, const PointF &half_res, + const Matrix4x4 &mat); + + Matrix4x4 + generate_auto_scaled_matrix(const Matrix4x4 &generated_matrix, + const NodeValueRow &db, const NodeGlobals &globals, + const VideoParams &texture_params) const; + + bool is_a_scale_gizmo(NodeGizmo *g) const; + + // Gizmo variables + double gizmo_start_angle_; + Matrix4x4 gizmo_inverted_transform_; + PointF gizmo_anchor_pt_; + bool gizmo_scale_uniform_; + double gizmo_last_angle_; + double gizmo_last_alt_angle_; + int gizmo_rotate_wrap_; + + enum RotationDirection { + k_direction_none, + k_direction_positive, // Clockwise + k_direction_negative // Counter-clockwise + }; + + static RotationDirection get_direction_from_angles(double last, + double current); + RotationDirection gizmo_rotate_last_dir_; + RotationDirection gizmo_rotate_last_alt_dir_; + + enum GizmoScaleType { k_gizmo_scale_x_only, k_gizmo_scale_y_only, k_gizmo_scale_both }; + + GizmoScaleType gizmo_scale_axes_; + Vector2D gizmo_scale_anchor_; + + // Gizmo on screen object storage + PointGizmo *point_gizmo_[k_gizmo_scale_count]; + PointGizmo *anchor_gizmo_; + PolygonGizmo *poly_gizmo_; + ScreenGizmo *rotation_gizmo_; +}; + +} + +#endif // OAK_TRANSFORMDISTORTNODE_H diff --git a/src/node/src/distort/wave/CMakeLists.txt b/src/node/src/distort/wave/CMakeLists.txt new file mode 100644 index 000000000..7e3720914 --- /dev/null +++ b/src/node/src/distort/wave/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/wave/wavedistortnode.cpp + node/distort/wave/wavedistortnode.h + PARENT_SCOPE +) diff --git a/src/node/src/distort/wave/wavedistortnode.cpp b/src/node/src/distort/wave/wavedistortnode.cpp new file mode 100644 index 000000000..24ea7fd69 --- /dev/null +++ b/src/node/src/distort/wave/wavedistortnode.cpp @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "wavedistortnode.h" + +namespace olive +{ + +const std::string WaveDistortNode::k_texture_input = "tex_in"; +const std::string WaveDistortNode::k_frequency_input = "frequency_in"; +const std::string WaveDistortNode::k_intensity_input = "intensity_in"; +const std::string WaveDistortNode::k_evolution_input = "evolution_in"; +const std::string WaveDistortNode::k_vertical_input = "vertical_in"; + +#define super Node + +WaveDistortNode::WaveDistortNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_frequency_input, NodeValue::k_float, 10); + add_input(k_intensity_input, NodeValue::k_float, 10); + add_input(k_evolution_input, NodeValue::k_float, 0); + + add_input(k_vertical_input, NodeValue::k_combo, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +std::string WaveDistortNode::name() const +{ + return "Wave"; +} + +std::string WaveDistortNode::id() const +{ + return "org.olivevideoeditor.Olive.wave"; +} + +std::vector WaveDistortNode::category() const +{ + return { k_category_distort }; +} + +std::string WaveDistortNode::description() const +{ + return "Distorts an image along a sine wave."; +} + +void WaveDistortNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_frequency_input, "Frequency"); + set_input_name(k_intensity_input, "Intensity"); + set_input_name(k_evolution_input, "Evolution"); + set_input_name(k_vertical_input, "Direction"); + set_combo_box_strings(k_vertical_input, { "Horizontal", "Vertical" }); +} + +ShaderCode WaveDistortNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/wave.frag")); +} + +void WaveDistortNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr texture = value.at(k_texture_input).to_texture()) { + // Only run shader if at least one of flip or flop are selected + if (value.at(k_intensity_input).to_double() != 0.0) { + table->push(NodeValue::k_texture, + Texture::job(texture->params(), ShaderJob(value)), + this); + } else { + // If we're not flipping or flopping just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +} diff --git a/src/node/src/distort/wave/wavedistortnode.h b/src/node/src/distort/wave/wavedistortnode.h new file mode 100644 index 000000000..8ba8b2a65 --- /dev/null +++ b/src/node/src/distort/wave/wavedistortnode.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_WAVEDISTORTNODE_H +#define OAK_WAVEDISTORTNODE_H + +#include "node.h" + +namespace olive +{ + +class WaveDistortNode : public Node { +public: + WaveDistortNode(); + + NODE_DEFAULT_FUNCTIONS(WaveDistortNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_frequency_input; + static const std::string k_intensity_input; + static const std::string k_evolution_input; + static const std::string k_vertical_input; +}; + +} + +#endif // OAK_WAVEDISTORTNODE_H diff --git a/src/node/src/effect/CMakeLists.txt b/src/node/src/effect/CMakeLists.txt new file mode 100644 index 000000000..4753fb991 --- /dev/null +++ b/src/node/src/effect/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(opacity) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/effect/opacity/CMakeLists.txt b/src/node/src/effect/opacity/CMakeLists.txt new file mode 100644 index 000000000..7d27380f9 --- /dev/null +++ b/src/node/src/effect/opacity/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/effect/opacity/opacityeffect.cpp + node/effect/opacity/opacityeffect.h + PARENT_SCOPE +) diff --git a/src/node/src/effect/opacity/opacityeffect.cpp b/src/node/src/effect/opacity/opacityeffect.cpp new file mode 100644 index 000000000..8f9d5cfb8 --- /dev/null +++ b/src/node/src/effect/opacity/opacityeffect.cpp @@ -0,0 +1,98 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "opacityeffect.h" + +#include +#include + +#include "math/math/math.h" +#include "sliderdisplaytype.h" + +#include "filefunctions.h" + +namespace olive +{ + +#define super Node + +const std::string OpacityEffect::k_texture_input = "tex_in"; +const std::string OpacityEffect::k_value_input = "opacity_in"; + +OpacityEffect::OpacityEffect() + : math_(new MathNode()) +{ + math_->set_operation(MathNode::k_op_multiply); + + set_node_position_in_context(math_.get(), PointF(0, 0)); + + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_value_input, NodeValue::k_float, 1.0); + set_input_property(k_value_input, "view", + slider::k_percentage); + set_input_property(k_value_input, "min", 0.0); + set_input_property(k_value_input, "max", 1.0); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +void OpacityEffect::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Texture"); + set_input_name(k_value_input, "Opacity"); +} + +ShaderCode OpacityEffect::get_shader_code(const ShaderRequest &request) const +{ + if (request.id == "rgbmult") { + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/opacity_rgb.frag")); + } else { + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/opacity.frag")); + } +} + +void OpacityEffect::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + if (TexturePtr opacity_tex = value.at(k_value_input).to_texture()) { + ShaderJob job(value); + job.set_shader_id("rgbmult"); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else if (std::abs(value.at(k_value_input).to_double() - 1.0) * + 1000000000000.0 > + std::min(std::abs(value.at(k_value_input).to_double()), + 1.0)) { + table->push(NodeValue::k_texture, tex->to_job(ShaderJob(value)), + this); + } else { + // 1.0 float is a no-op, so just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +} diff --git a/src/node/src/effect/opacity/opacityeffect.h b/src/node/src/effect/opacity/opacityeffect.h new file mode 100644 index 000000000..9f560dd8e --- /dev/null +++ b/src/node/src/effect/opacity/opacityeffect.h @@ -0,0 +1,73 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_OPACITYEFFECT_H +#define OAK_OPACITYEFFECT_H + +#include + +#include "group/group.h" +#include "math/math/math.h" + +namespace olive +{ + +class OpacityEffect : public Node { +public: + OpacityEffect(); + + NODE_DEFAULT_FUNCTIONS(OpacityEffect) + + virtual std::string name() const override + { + return "Opacity"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.opacity"; + } + + virtual std::vector category() const override + { + return { k_category_filter }; + } + + virtual std::string description() const override + { + return "Alter a video's opacity.\n\nThis is equivalent to multiplying a video by a number between 0.0 and 1.0."; + } + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_value_input; + +private: + // Owned child node (replaces the former QObject child via setParent(this)) + std::unique_ptr math_; +}; + +} + +#endif // OAK_OPACITYEFFECT_H diff --git a/src/node/src/factory.cpp b/src/node/src/factory.cpp new file mode 100644 index 000000000..a86e11002 --- /dev/null +++ b/src/node/src/factory.cpp @@ -0,0 +1,313 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "factory.h" + +#include +#include +#include + +#include "audio/pan/pan.h" +#include "audio/volume/volume.h" +#include "block/clip/clip.h" +#include "block/gap/gap.h" +#include "block/subtitle/subtitle.h" +#include "block/transition/crossdissolve/crossdissolvetransition.h" +#include "block/transition/diptocolor/diptocolortransition.h" +#include "color/displaytransform/displaytransform.h" +#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h" +#include "color/ociogradingtransformlog/ociogradingtransformlog.h" +#include "color/ociolut/ociolut.h" +#include "color/threewaycolor/threewaycolor.h" +#include "color/whitebalance/whitebalance.h" +#include "current.h" +#include "distort/cornerpin/cornerpindistortnode.h" +#include "distort/crop/cropdistortnode.h" +#include "distort/flip/flipdistortnode.h" +#include "distort/mask/mask.h" +#include "distort/ripple/rippledistortnode.h" +#include "distort/swirl/swirldistortnode.h" +#include "distort/tile/tiledistortnode.h" +#include "distort/transform/transformdistortnode.h" +#include "distort/wave/wavedistortnode.h" +#include "effect/opacity/opacityeffect.h" +#include "filter/blur/blur.h" +#include "filter/dropshadow/dropshadowfilter.h" +#include "filter/mosaic/mosaicfilternode.h" +#include "filter/stroke/stroke.h" +#include "generator/matrix/matrix.h" +#include "generator/noise/noise.h" +#include "generator/polygon/polygon.h" +#include "generator/shape/shapenode.h" +#include "generator/solid/solid.h" +#include "generator/text/textv1.h" +#include "generator/text/textv2.h" +#include "generator/text/textv3.h" +#include "input/multicam/multicamnode.h" +#include "input/time/timeinput.h" +#include "input/value/valuenode.h" +#include "keying/chromakey/chromakey.h" +#include "keying/colordifferencekey/colordifferencekey.h" +#include "keying/despill/despill.h" +#include "math/math/math.h" +#include "math/merge/merge.h" +#include "math/trigonometry/trigonometry.h" +#include "output/track/track.h" +#include "output/viewer/viewer.h" +#include "pluginSupport/olivehost.h" +#include "plugins/plugin.h" +#include "project/folder/folder.h" +#include "project/footage/footage.h" +#include "project/sequence/sequence.h" +#include "time/timeformat/timeformat.h" +#include "time/timeoffset/timeoffsetnode.h" +#include "time/timeremap/timeremap.h" + +namespace olive +{ + +std::vector NodeFactory::library; + +void NodeFactory::initialize() +{ + destroy(); + + // Add internal types + for (int i = 0; i < k_internal_node_count; i++) { + Node *created_node = create_from_factory_index(static_cast(i)); + + library.push_back(created_node); + } + + register_plugin_nodes(); +} + +void NodeFactory::destroy() +{ + for (Node *n : library) { + delete n; + } + library.clear(); +} + +std::string NodeFactory::get_name_from_id(const std::string &id) +{ + if (!id.empty()) { + for (Node *n : library) { + if (n->id() == id) { + return n->name(); + } + } + } + + return std::string(); +} + +Node *NodeFactory::create_from_id(const std::string &id) +{ + std::string resolved_id = id; + + // Node IDs renamed after older project files were written + static const std::map k_legacy_i_ds = { + { "org.oliveeditor.Olive.flip", + "org.olivevideoeditor.Olive.flip" }, + { "org.oliveeditor.Olive.ripple", + "org.olivevideoeditor.Olive.ripple" }, + { "org.oliveeditor.Olive.swirl", + "org.olivevideoeditor.Olive.swirl" }, + { "org.oliveeditor.Olive.tile", + "org.olivevideoeditor.Olive.tile" }, + { "org.oliveeditor.Olive.wave", + "org.olivevideoeditor.Olive.wave" }, + }; + auto legacy_it = k_legacy_i_ds.find(id); + if (legacy_it != k_legacy_i_ds.end()) { + resolved_id = legacy_it->second; + } + + for (Node *n : library) { + if (n->id() == resolved_id) { + return n->copy(); + } + } + + return nullptr; +} + +void NodeFactory::register_plugin_nodes() +{ + std::set existing_ids; + for (Node *node : library) { + existing_ids.insert(node->id()); + } + + for (auto plugin : OFX::Host::PluginCache::getPluginCache()->getPlugins()) { + auto *image_effect = + dynamic_cast(plugin); + if (!image_effect) { + continue; + } + + const std::string plugin_id = image_effect->getIdentifier(); + if (existing_ids.count(plugin_id)) { + continue; + } + + const auto &contexts = image_effect->getContexts(); + if (contexts.empty()) { + fprintf(stderr, "Skipping OFX plugin with no contexts: %s\n", + plugin_id.c_str()); + continue; + } + std::string context = kOfxImageEffectContextFilter; + if (contexts.find(kOfxImageEffectContextFilter) == contexts.end()) { + context = *contexts.begin(); + } + + auto *instance = image_effect->createInstance(context, nullptr); + if (!instance) { + continue; + } + + plugin::PluginNode *plugin_node = new plugin::PluginNode(instance); + library.push_back(plugin_node); + existing_ids.insert(plugin_id); + } +} + +Node *NodeFactory::create_from_factory_index(const NodeFactory::InternalID &id) +{ + switch (id) { + case k_clip_block: + return new ClipBlock(); + case k_gap_block: + return new GapBlock(); + case k_polygon_generator: + return new PolygonGenerator(); + case k_matrix_generator: + return new MatrixGenerator(); + case k_transform_distort: + return new TransformDistortNode(); + case k_track_output: + return new Track(); + case k_viewer_output: + return new ViewerOutput(); + case k_audio_volume: + return new VolumeNode(); + case k_audio_panning: + return new PanNode(); + case k_math: + return new MathNode(); + case k_trigonometry: + return new TrigonometryNode(); + case k_time: + return new TimeInput(); + case k_blur_filter: + return new BlurFilterNode(); + case k_solid_generator: + return new SolidGenerator(); + case k_merge: + return new MergeNode(); + case k_stroke_filter: + return new StrokeFilterNode(); + case k_text_generator_v1: + return new TextGeneratorV1(); + case k_text_generator_v2: + return new TextGeneratorV2(); + case k_text_generator_v3: + return new TextGeneratorV3(); + case k_cross_dissolve_transition: + return new CrossDissolveTransition(); + case k_dip_to_color_transition: + return new DipToColorTransition(); + case k_mosaic_filter: + return new MosaicFilterNode(); + case k_crop_distort: + return new CropDistortNode(); + case k_project_footage: + return new Footage(); + case k_project_folder: + return new Folder(); + case k_project_sequence: + return new Sequence(); + case k_value_node: + return new ValueNode(); + case k_time_remap_node: + return new TimeRemapNode(); + case k_subtitle_block: + return new SubtitleBlock(); + case k_shape_generator: + return new ShapeNode(); + case k_color_difference_key_keying: + return new ColorDifferenceKeyNode(); + case k_despill_keying: + return new DespillNode(); + case k_group_node: + return new NodeGroup(); + case k_opacity_effect: + return new OpacityEffect(); + case k_flip_distort: + return new FlipDistortNode(); + case k_noise_generator: + return new NoiseGeneratorNode(); + case k_time_offset_node: + return new TimeOffsetNode(); + case k_corner_pin_distort: + return new CornerPinDistortNode(); + case k_display_transform: + return new DisplayTransformNode(); + case k_ocio_grading_transform_linear: + return new OCIOGradingTransformLinearNode(); + case k_ocio_grading_transform_log: + return new OCIOGradingTransformLogNode(); + case k_white_balance: + return new WhiteBalanceNode(); + case k_ocio_lut: + return new OCIOLutNode(); + case k_three_way_color: + return new ThreeWayColorNode(); + case k_chroma_key: + return new ChromaKeyNode(); + case k_mask_distort: + return new MaskDistortNode(); + case k_drop_shadow_filter: + return new DropShadowFilter(); + case k_time_format: + return new TimeFormatNode(); + case k_wave_distort: + return new WaveDistortNode(); + case k_tile_distort: + return new TileDistortNode(); + case k_swirl_distort: + return new SwirlDistortNode(); + case k_ripple_distort: + return new RippleDistortNode(); + case k_multicam_node: + return new MultiCamNode(); + + case k_internal_node_count: + break; + } + + return nullptr; +} + +} diff --git a/src/node/src/factory.h b/src/node/src/factory.h new file mode 100644 index 000000000..80c04fe52 --- /dev/null +++ b/src/node/src/factory.h @@ -0,0 +1,125 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEFACTORY_H +#define OAK_NODEFACTORY_H + +#include +#include + +#include "node.h" + +namespace olive +{ + +class NodeFactory { +public: + enum InternalID { + k_viewer_output, + k_clip_block, + k_gap_block, + k_polygon_generator, + k_matrix_generator, + k_transform_distort, + k_track_output, + k_audio_volume, + k_audio_panning, + k_math, + k_time, + k_trigonometry, + k_blur_filter, + k_solid_generator, + k_merge, + k_stroke_filter, + k_text_generator_v1, + k_text_generator_v2, + k_text_generator_v3, + k_cross_dissolve_transition, + k_dip_to_color_transition, + k_mosaic_filter, + k_crop_distort, + k_project_footage, + k_project_folder, + k_project_sequence, + k_value_node, + k_time_remap_node, + k_subtitle_block, + k_shape_generator, + k_color_difference_key_keying, + k_despill_keying, + k_group_node, + k_opacity_effect, + k_flip_distort, + k_noise_generator, + k_time_offset_node, + k_corner_pin_distort, + k_display_transform, + k_ocio_grading_transform_linear, + k_ocio_lut, + k_three_way_color, + k_chroma_key, + k_mask_distort, + k_drop_shadow_filter, + k_time_format, + k_wave_distort, + k_ripple_distort, + k_tile_distort, + k_swirl_distort, + k_multicam_node, + k_ocio_grading_transform_log, + k_white_balance, + + // Count value + k_internal_node_count + }; + + NodeFactory() = default; + + static void initialize(); + + static void destroy(); + + static std::string get_name_from_id(const std::string &id); + + static Node *create_from_id(const std::string &id); + static void register_plugin_nodes(); + + static Node *create_from_factory_index(const InternalID &id); + + /** + * @brief Access the internal node library + * + * Exposed for the UI layer (e.g. widget/menu/factorymenu), which builds + * node creation menus from the library. The library itself stays + * UI-independent. + */ + static const std::vector &get_library() + { + return library; + } + +private: + static std::vector library; +}; + +} + +#endif // OAK_NODEFACTORY_H diff --git a/src/node/src/filter/CMakeLists.txt b/src/node/src/filter/CMakeLists.txt new file mode 100644 index 000000000..512a56b3b --- /dev/null +++ b/src/node/src/filter/CMakeLists.txt @@ -0,0 +1,25 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(blur) +add_subdirectory(dropshadow) +add_subdirectory(mosaic) +add_subdirectory(stroke) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/filter/blur/CMakeLists.txt b/src/node/src/filter/blur/CMakeLists.txt new file mode 100644 index 000000000..b38acb0e1 --- /dev/null +++ b/src/node/src/filter/blur/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/filter/blur/blur.h + node/filter/blur/blur.cpp + PARENT_SCOPE +) diff --git a/src/node/src/filter/blur/blur.cpp b/src/node/src/filter/blur/blur.cpp new file mode 100644 index 000000000..7729ce28b --- /dev/null +++ b/src/node/src/filter/blur/blur.cpp @@ -0,0 +1,237 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "blur.h" + +#include "filefunctions.h" + +namespace olive +{ + +const std::string BlurFilterNode::k_texture_input = "tex_in"; +const std::string BlurFilterNode::k_method_input = "method_in"; +const std::string BlurFilterNode::k_radius_input = "radius_in"; +const std::string BlurFilterNode::k_horiz_input = "horiz_in"; +const std::string BlurFilterNode::k_vert_input = "vert_in"; +const std::string BlurFilterNode::k_repeat_edge_pixels_input = + "repeat_edge_pixels_in"; + +const std::string BlurFilterNode::k_directional_degrees_input = + "directional_degrees_in"; + +const std::string BlurFilterNode::k_radial_center_input = "radial_center_in"; + +#define super Node + +BlurFilterNode::BlurFilterNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + Method default_method = k_gaussian; + + add_input(k_method_input, NodeValue::k_combo, default_method, + InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable)); + + add_input(k_radius_input, NodeValue::k_float, 10.0); + set_input_property(k_radius_input, "min", 0.0); + + { + // Box and gaussian only + add_input(k_horiz_input, NodeValue::k_boolean, true); + add_input(k_vert_input, NodeValue::k_boolean, true); + } + + { + // Directional only + add_input(k_directional_degrees_input, NodeValue::k_float, 0.0); + } + + { + // Radial only + add_input(k_radial_center_input, NodeValue::k_vec2, Vector2D(0, 0)); + } + + update_inputs(default_method); + + add_input(k_repeat_edge_pixels_input, NodeValue::k_boolean, true); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); + + radial_center_gizmo_ = add_draggable_gizmo(); + radial_center_gizmo_->set_shape(PointGizmo::k_anchor_point); + radial_center_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_radial_center_input), 0)); + radial_center_gizmo_->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_radial_center_input), 1)); +} + +std::string BlurFilterNode::name() const +{ + return "Blur"; +} + +std::string BlurFilterNode::id() const +{ + return "org.olivevideoeditor.Olive.blur"; +} + +std::vector BlurFilterNode::category() const +{ + return { k_category_filter }; +} + +std::string BlurFilterNode::description() const +{ + return "Blurs an image."; +} + +void BlurFilterNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_method_input, "Method"); + set_combo_box_strings(k_method_input, + { "Box", "Gaussian", "Directional", "Radial" }); + set_input_name(k_radius_input, "Radius"); + set_input_name(k_horiz_input, "Horizontal"); + set_input_name(k_vert_input, "Vertical"); + set_input_name(k_repeat_edge_pixels_input, "Repeat Edge Pixels"); + + set_input_name(k_directional_degrees_input, "Direction"); + set_input_name(k_radial_center_input, "Center"); +} + +ShaderCode BlurFilterNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/blur.frag")); +} + +void BlurFilterNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + Method method = static_cast(value.at(k_method_input).to_int()); + + bool can_push_job = true; + int iterations = 1; + + // Check if radius is > 0 + if (value.at(k_radius_input).to_double() > 0.0) { + // Method-specific considerations + switch (method) { + case k_box: + case k_gaussian: { + bool horiz = value.at(k_horiz_input).to_bool(); + bool vert = value.at(k_vert_input).to_bool(); + + if (!horiz && !vert) { + // Disable job if horiz and vert are unchecked + can_push_job = false; + } else if (horiz && vert) { + // Set iteration count to 2 if we're blurring both horizontally and vertically + iterations = 2; + } + break; + } + case k_directional: + case k_radial: + break; + } + } else { + can_push_job = false; + } + + if (can_push_job) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + job.set_iterations(iterations, k_texture_input); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + // If we're not performing the blur job, just push the texture + table->push(value.at(k_texture_input)); + } + } +} + +void BlurFilterNode::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + if (TexturePtr tex = row.at(k_texture_input).to_texture()) { + if (row.at(k_method_input).to_int() == k_radial) { + const Vector2D &sequence_res = tex->virtual_resolution(); + Vector2D sequence_half_res = sequence_res * 0.5; + + radial_center_gizmo_->set_visible(true); + radial_center_gizmo_->set_point( + sequence_half_res.to_point_f() + + row.at(k_radial_center_input).to_vec2().to_point_f()); + + set_input_property(k_radial_center_input, "offset", + sequence_half_res); + } else { + radial_center_gizmo_->set_visible(false); + } + } +} + +void BlurFilterNode::gizmo_drag_move(double x, double y, int modifiers) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + if (gizmo == radial_center_gizmo_) { + NodeInputDragger &x_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo->get_draggers()[1]; + + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); + } +} + +void BlurFilterNode::InputValueChangedEvent(const std::string &input, + int element) +{ + if (input == k_method_input) { + update_inputs(get_method()); + } + + super::InputValueChangedEvent(input, element); +} + +void BlurFilterNode::update_inputs(Method method) +{ + set_input_flag(k_horiz_input, k_input_flag_hidden, + !(method == k_box || method == k_gaussian)); + set_input_flag(k_vert_input, k_input_flag_hidden, + !(method == k_box || method == k_gaussian)); + set_input_flag(k_directional_degrees_input, k_input_flag_hidden, + !(method == k_directional)); + set_input_flag(k_radial_center_input, k_input_flag_hidden, !(method == k_radial)); +} + +} diff --git a/src/node/src/filter/blur/blur.h b/src/node/src/filter/blur/blur.h new file mode 100644 index 000000000..339f4103c --- /dev/null +++ b/src/node/src/filter/blur/blur.h @@ -0,0 +1,84 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_BLURFILTERNODE_H +#define OAK_BLURFILTERNODE_H + +#include "gizmo/point.h" +#include "node.h" + +namespace olive +{ + +class BlurFilterNode : public Node { +public: + BlurFilterNode(); + + enum Method { k_box, k_gaussian, k_directional, k_radial }; + + NODE_DEFAULT_FUNCTIONS(BlurFilterNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + Method get_method() const + { + return static_cast(get_standard_value(k_method_input).to_int()); + } + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + static const std::string k_texture_input; + static const std::string k_method_input; + static const std::string k_radius_input; + static const std::string k_horiz_input; + static const std::string k_vert_input; + static const std::string k_repeat_edge_pixels_input; + + static const std::string k_directional_degrees_input; + + static const std::string k_radial_center_input; + +protected: + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + +private: + void update_inputs(Method method); + + PointGizmo *radial_center_gizmo_; +}; + +} + +#endif // OAK_BLURFILTERNODE_H diff --git a/src/node/src/filter/dropshadow/CMakeLists.txt b/src/node/src/filter/dropshadow/CMakeLists.txt new file mode 100644 index 000000000..6ed873067 --- /dev/null +++ b/src/node/src/filter/dropshadow/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/filter/dropshadow/dropshadowfilter.h + node/filter/dropshadow/dropshadowfilter.cpp + PARENT_SCOPE +) diff --git a/src/node/src/filter/dropshadow/dropshadowfilter.cpp b/src/node/src/filter/dropshadow/dropshadowfilter.cpp new file mode 100644 index 000000000..4ce9a96f6 --- /dev/null +++ b/src/node/src/filter/dropshadow/dropshadowfilter.cpp @@ -0,0 +1,107 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "dropshadowfilter.h" + +#include "filefunctions.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +#define super Node + +const std::string DropShadowFilter::k_texture_input = "tex_in"; +const std::string DropShadowFilter::k_color_input = "color_in"; +const std::string DropShadowFilter::k_distance_input = "distance_in"; +const std::string DropShadowFilter::k_angle_input = "angle_in"; +const std::string DropShadowFilter::k_softness_input = "radius_in"; +const std::string DropShadowFilter::k_opacity_input = "opacity_in"; +const std::string DropShadowFilter::k_fast_input = "fast_in"; + +DropShadowFilter::DropShadowFilter() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(0.0, 0.0, 0.0))); + + add_input(k_distance_input, NodeValue::k_float, 10.0); + + add_input(k_angle_input, NodeValue::k_float, 135.0); + + add_input(k_softness_input, NodeValue::k_float, 10.0); + set_input_property(k_softness_input, "min", 0.0); + + add_input(k_opacity_input, NodeValue::k_float, 1.0); + set_input_property(k_opacity_input, "min", 0.0); + set_input_property(k_opacity_input, "view", slider::k_percentage); + + add_input(k_fast_input, NodeValue::k_boolean, false); + + set_effect_input(k_texture_input); + set_flag(k_video_effect); +} + +void DropShadowFilter::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Texture"); + set_input_name(k_color_input, "Color"); + set_input_name(k_distance_input, "Distance"); + set_input_name(k_angle_input, "Angle"); + set_input_name(k_softness_input, "Softness"); + set_input_name(k_opacity_input, "Opacity"); + set_input_name(k_fast_input, "Faster (Lower Quality)"); +} + +ShaderCode DropShadowFilter::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/dropshadow.frag")); +} + +void DropShadowFilter::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + ShaderJob job(value); + + std::string iterative = "previous_iteration_in"; + + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + job.insert(iterative, value.at(k_texture_input)); + + if (value.at(k_softness_input).to_double() != 0.0) { + job.set_iterations(3, iterative); + } + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} + +} diff --git a/src/node/src/filter/dropshadow/dropshadowfilter.h b/src/node/src/filter/dropshadow/dropshadowfilter.h new file mode 100644 index 000000000..3178f9302 --- /dev/null +++ b/src/node/src/filter/dropshadow/dropshadowfilter.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DROPSHADOWFILTER_H +#define OAK_DROPSHADOWFILTER_H + +#include "node.h" + +namespace olive +{ + +class DropShadowFilter : public Node { +public: + DropShadowFilter(); + + NODE_DEFAULT_FUNCTIONS(DropShadowFilter) + + virtual std::string name() const override + { + return "Drop Shadow"; + } + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.dropshadow"; + } + virtual std::vector category() const override + { + return { k_category_filter }; + } + virtual std::string description() const override + { + return "Adds a drop shadow to an image."; + } + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_color_input; + static const std::string k_distance_input; + static const std::string k_angle_input; + static const std::string k_softness_input; + static const std::string k_opacity_input; + static const std::string k_fast_input; +}; + +} + +#endif // OAK_DROPSHADOWFILTER_H diff --git a/src/node/src/filter/mosaic/CMakeLists.txt b/src/node/src/filter/mosaic/CMakeLists.txt new file mode 100644 index 000000000..9388b6bf8 --- /dev/null +++ b/src/node/src/filter/mosaic/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/filter/mosaic/mosaicfilternode.h + node/filter/mosaic/mosaicfilternode.cpp + PARENT_SCOPE +) diff --git a/src/node/src/filter/mosaic/mosaicfilternode.cpp b/src/node/src/filter/mosaic/mosaicfilternode.cpp new file mode 100644 index 000000000..564d64c6c --- /dev/null +++ b/src/node/src/filter/mosaic/mosaicfilternode.cpp @@ -0,0 +1,85 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "mosaicfilternode.h" + +#include "filefunctions.h" + +namespace olive +{ + +const std::string MosaicFilterNode::k_texture_input = "tex_in"; +const std::string MosaicFilterNode::k_horiz_input = "horiz_in"; +const std::string MosaicFilterNode::k_vert_input = "vert_in"; + +#define super Node + +MosaicFilterNode::MosaicFilterNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_horiz_input, NodeValue::k_float, 32.0); + set_input_property(k_horiz_input, "min", 1.0); + + add_input(k_vert_input, NodeValue::k_float, 18.0); + set_input_property(k_vert_input, "min", 1.0); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +void MosaicFilterNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Texture"); + set_input_name(k_horiz_input, "Horizontal"); + set_input_name(k_vert_input, "Vertical"); +} + +void MosaicFilterNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr texture = value.at(k_texture_input).to_texture()) { + if (texture && (value.at(k_horiz_input).to_int() != texture->width() || + value.at(k_vert_input).to_int() != texture->height())) { + ShaderJob job(value); + + // Mipmapping makes this look weird, so we just use bilinear for finding the color of each block + job.set_interpolation(k_texture_input, Texture::k_linear); + + table->push(NodeValue::k_texture, texture->to_job(job), this); + } else { + table->push(value.at(k_texture_input)); + } + } +} + +ShaderCode MosaicFilterNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/mosaic.frag")); +} + +} diff --git a/src/node/src/filter/mosaic/mosaicfilternode.h b/src/node/src/filter/mosaic/mosaicfilternode.h new file mode 100644 index 000000000..983cfbb36 --- /dev/null +++ b/src/node/src/filter/mosaic/mosaicfilternode.h @@ -0,0 +1,70 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MOSAICFILTERNODE_H +#define OAK_MOSAICFILTERNODE_H + +#include "node.h" + +namespace olive +{ + +class MosaicFilterNode : public Node { +public: + MosaicFilterNode(); + + NODE_DEFAULT_FUNCTIONS(MosaicFilterNode) + + virtual std::string name() const override + { + return "Mosaic"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.mosaicfilter"; + } + + virtual std::vector category() const override + { + return { k_category_filter }; + } + + virtual std::string description() const override + { + return "Apply a pixelated mosaic filter to video."; + } + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + static const std::string k_texture_input; + static const std::string k_horiz_input; + static const std::string k_vert_input; +}; + +} + +#endif // OAK_MOSAICFILTERNODE_H diff --git a/src/node/src/filter/stroke/CMakeLists.txt b/src/node/src/filter/stroke/CMakeLists.txt new file mode 100644 index 000000000..8117b28d7 --- /dev/null +++ b/src/node/src/filter/stroke/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/filter/stroke/stroke.h + node/filter/stroke/stroke.cpp + PARENT_SCOPE +) diff --git a/src/node/src/filter/stroke/stroke.cpp b/src/node/src/filter/stroke/stroke.cpp new file mode 100644 index 000000000..bc77cb83f --- /dev/null +++ b/src/node/src/filter/stroke/stroke.cpp @@ -0,0 +1,116 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "stroke.h" + +#include "filefunctions.h" +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string StrokeFilterNode::k_texture_input = "tex_in"; +const std::string StrokeFilterNode::k_color_input = "color_in"; +const std::string StrokeFilterNode::k_radius_input = "radius_in"; +const std::string StrokeFilterNode::k_opacity_input = "opacity_in"; +const std::string StrokeFilterNode::k_inner_input = "inner_in"; + +#define super Node + +StrokeFilterNode::StrokeFilterNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(1.0f, 1.0f, 1.0f, 1.0f))); + + add_input(k_radius_input, NodeValue::k_float, 10.0); + set_input_property(k_radius_input, "min", 0.0); + + add_input(k_opacity_input, NodeValue::k_float, 1.0f); + set_input_property(k_opacity_input, "view", slider::k_percentage); + set_input_property(k_opacity_input, "min", 0.0f); + set_input_property(k_opacity_input, "max", 1.0f); + + add_input(k_inner_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +std::string StrokeFilterNode::name() const +{ + return "Stroke"; +} + +std::string StrokeFilterNode::id() const +{ + return "org.olivevideoeditor.Olive.stroke"; +} + +std::vector StrokeFilterNode::category() const +{ + return { k_category_filter }; +} + +std::string StrokeFilterNode::description() const +{ + return "Creates a stroke outline around an image."; +} + +void StrokeFilterNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_color_input, "Color"); + set_input_name(k_radius_input, "Radius"); + set_input_name(k_opacity_input, "Opacity"); + set_input_name(k_inner_input, "Inner"); +} + +void StrokeFilterNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + if (value.at(k_radius_input).to_double() > 0.0 && + value.at(k_opacity_input).to_double() > 0.0) { + ShaderJob job(value); + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, tex->virtual_resolution(), + this)); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } else { + table->push(value.at(k_texture_input)); + } + } +} + +ShaderCode StrokeFilterNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/stroke.frag")); +} + +} diff --git a/src/node/src/filter/stroke/stroke.h b/src/node/src/filter/stroke/stroke.h new file mode 100644 index 000000000..c00118d83 --- /dev/null +++ b/src/node/src/filter/stroke/stroke.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_STROKEFILTERNODE_H +#define OAK_STROKEFILTERNODE_H + +#include "node.h" + +namespace olive +{ + +class StrokeFilterNode : public Node { +public: + StrokeFilterNode(); + + NODE_DEFAULT_FUNCTIONS(StrokeFilterNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + static const std::string k_texture_input; + static const std::string k_color_input; + static const std::string k_radius_input; + static const std::string k_opacity_input; + static const std::string k_inner_input; +}; + +} + +#endif // OAK_STROKEFILTERNODE_H diff --git a/src/node/src/generator/CMakeLists.txt b/src/node/src/generator/CMakeLists.txt new file mode 100644 index 000000000..bd854bba4 --- /dev/null +++ b/src/node/src/generator/CMakeLists.txt @@ -0,0 +1,27 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(matrix) +add_subdirectory(noise) +add_subdirectory(polygon) +add_subdirectory(shape) +add_subdirectory(solid) +add_subdirectory(text) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/generator/matrix/CMakeLists.txt b/src/node/src/generator/matrix/CMakeLists.txt new file mode 100644 index 000000000..48d917e90 --- /dev/null +++ b/src/node/src/generator/matrix/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/matrix/matrix.h + node/generator/matrix/matrix.cpp + PARENT_SCOPE +) diff --git a/src/node/src/generator/matrix/matrix.cpp b/src/node/src/generator/matrix/matrix.cpp new file mode 100644 index 000000000..e0f32d9a8 --- /dev/null +++ b/src/node/src/generator/matrix/matrix.cpp @@ -0,0 +1,161 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "matrix.h" + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string MatrixGenerator::k_position_input = "pos_in"; +const std::string MatrixGenerator::k_rotation_input = "rot_in"; +const std::string MatrixGenerator::k_scale_input = "scale_in"; +const std::string MatrixGenerator::k_uniform_scale_input = "uniform_scale_in"; +const std::string MatrixGenerator::k_anchor_input = "anchor_in"; + +#define super Node + +MatrixGenerator::MatrixGenerator() +{ + add_input(k_position_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); + + add_input(k_rotation_input, NodeValue::k_float, 0.0); + + add_input(k_scale_input, NodeValue::k_vec2, Vector2D(1.0f, 1.0f)); + set_input_property(k_scale_input, "min", Vector2D(0, 0)); + set_input_property(k_scale_input, "view", slider::k_percentage); + set_input_property(k_scale_input, "disable1", true); + + add_input(k_uniform_scale_input, NodeValue::k_boolean, true, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_anchor_input, NodeValue::k_vec2, Vector2D(0.0, 0.0)); +} + +std::string MatrixGenerator::name() const +{ + return "Orthographic Matrix"; +} + +std::string MatrixGenerator::short_name() const +{ + return "Ortho"; +} + +std::string MatrixGenerator::id() const +{ + return "org.olivevideoeditor.Olive.ortho"; +} + +std::vector MatrixGenerator::category() const +{ + return { k_category_generator, k_category_math }; +} + +std::string MatrixGenerator::description() const +{ + return "Generate an orthographic matrix using position, rotation, and scale."; +} + +void MatrixGenerator::retranslate() +{ + super::retranslate(); + + set_input_name(k_position_input, "Position"); + set_input_name(k_rotation_input, "Rotation"); + set_input_name(k_scale_input, "Scale"); + set_input_name(k_uniform_scale_input, "Uniform Scale"); + set_input_name(k_anchor_input, "Anchor Point"); +} + +void MatrixGenerator::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Push matrix output + Matrix4x4 mat = generate_matrix(value, false, false, false, Matrix4x4()); + table->push(NodeValue::k_matrix, mat, this); +} + +Matrix4x4 MatrixGenerator::generate_matrix(const NodeValueRow &value, + bool ignore_anchor, + bool ignore_position, + bool ignore_scale, + const Matrix4x4 &mat) const +{ + Vector2D anchor; + Vector2D position; + Vector2D scale; + + if (!ignore_anchor) { + anchor = value.at(k_anchor_input).to_vec2(); + } + + if (!ignore_scale) { + scale = value.at(k_scale_input).to_vec2(); + } + + if (!ignore_position) { + position = value.at(k_position_input).to_vec2(); + } + + return generate_matrix(position, value.at(k_rotation_input).to_double(), scale, + value.at(k_uniform_scale_input).to_bool(), anchor, mat); +} + +Matrix4x4 +MatrixGenerator::generate_matrix(const Vector2D &pos, const float &rot, + const Vector2D &scale, bool uniform_scale, + const Vector2D &anchor, Matrix4x4 mat) +{ + // Position + mat.translate(pos.x(), pos.y()); + + // Rotation (2D rotation around the Z axis, same as QMatrix4x4::rotate(rot, 0, 0, 1)) + mat.rotate(rot); + + // Scale (convert to a Vector3D so that the identity matrix is preserved if all values are 1.0f) + Vector3D full_scale; + if (uniform_scale) { + full_scale = Vector3D(scale.x(), scale.x(), 1.0f); + } else { + full_scale = Vector3D(scale.x(), scale.y(), 1.0f); + } + mat.scale(full_scale.x(), full_scale.y(), full_scale.z()); + + // Anchor Point + mat.translate(-anchor.x(), -anchor.y()); + + return mat; +} + +void MatrixGenerator::InputValueChangedEvent(const std::string &input, int element) +{ + (void) element; + + if (input == k_uniform_scale_input) { + set_input_property(k_scale_input, "disable1", + get_standard_value(k_uniform_scale_input).to_bool()); + } +} + +} diff --git a/src/node/src/generator/matrix/matrix.h b/src/node/src/generator/matrix/matrix.h new file mode 100644 index 000000000..33916fbae --- /dev/null +++ b/src/node/src/generator/matrix/matrix.h @@ -0,0 +1,69 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MATRIXGENERATOR_H +#define OAK_MATRIXGENERATOR_H + +#include "mathtypes.h" +#include "node.h" +#include "inputdragger.h" + +namespace olive +{ + +class MatrixGenerator : public Node { +public: + MatrixGenerator(); + + NODE_DEFAULT_FUNCTIONS(MatrixGenerator) + + virtual std::string name() const override; + virtual std::string short_name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_position_input; + static const std::string k_rotation_input; + static const std::string k_scale_input; + static const std::string k_uniform_scale_input; + static const std::string k_anchor_input; + +protected: + Matrix4x4 generate_matrix(const NodeValueRow &value, bool ignore_anchor, + bool ignore_position, bool ignore_scale, + const Matrix4x4 &mat) const; + static Matrix4x4 generate_matrix(const Vector2D &pos, const float &rot, + const Vector2D &scale, bool uniform_scale, + const Vector2D &anchor, Matrix4x4 mat); + + virtual void InputValueChangedEvent(const std::string &input, + int element) override; +}; + +} + +#endif // TRANSFORMDISTORT_H diff --git a/src/node/src/generator/noise/CMakeLists.txt b/src/node/src/generator/noise/CMakeLists.txt new file mode 100644 index 000000000..592f05579 --- /dev/null +++ b/src/node/src/generator/noise/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/noise/noise.h + node/generator/noise/noise.cpp + PARENT_SCOPE +) diff --git a/src/node/src/generator/noise/noise.cpp b/src/node/src/generator/noise/noise.cpp new file mode 100644 index 000000000..9169cbb4c --- /dev/null +++ b/src/node/src/generator/noise/noise.cpp @@ -0,0 +1,101 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "noise.h" + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string NoiseGeneratorNode::k_base_in = "base_in"; +const std::string NoiseGeneratorNode::k_color_input = "color_in"; +const std::string NoiseGeneratorNode::k_strength_input = "strength_in"; + +#define super Node + +NoiseGeneratorNode::NoiseGeneratorNode() +{ + add_input(k_base_in, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_strength_input, NodeValue::k_float, 0.2); + set_input_property(k_strength_input, "view", slider::k_percentage); + set_input_property(k_strength_input, "min", 0); + + add_input(k_color_input, NodeValue::k_boolean, false); + + set_effect_input(k_base_in); + set_flag(k_video_effect); +} + +std::string NoiseGeneratorNode::name() const +{ + return "Noise"; +} + +std::string NoiseGeneratorNode::id() const +{ + return "org.olivevideoeditor.Olive.noise"; +} + +std::vector NoiseGeneratorNode::category() const +{ + return { k_category_generator }; +} + +std::string NoiseGeneratorNode::description() const +{ + return "Generates noise patterns"; +} + +void NoiseGeneratorNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_base_in, "Base"); + set_input_name(k_strength_input, "Strength"); + set_input_name(k_color_input, "Color"); +} + +ShaderCode NoiseGeneratorNode::get_shader_code(const ShaderRequest &request) const +{ + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/noise.frag")); +} + +void NoiseGeneratorNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + ShaderJob job(value); + + job.insert(value); + job.insert("time_in", + NodeValue(NodeValue::k_float, globals.time().in().to_double(), + this)); + + TexturePtr base = value.at(k_base_in).to_texture(); + + table->push(NodeValue::k_texture, + Texture::job(base ? base->params() : globals.vparams(), job), + this); +} +} diff --git a/src/node/src/generator/noise/noise.h b/src/node/src/generator/noise/noise.h new file mode 100644 index 000000000..8cff2f41a --- /dev/null +++ b/src/node/src/generator/noise/noise.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NOISEGENERATORNODE_H +#define OAK_NOISEGENERATORNODE_H + +#include "node.h" + +namespace olive +{ + +class NoiseGeneratorNode : public Node { +public: + NoiseGeneratorNode(); + + NODE_DEFAULT_FUNCTIONS(NoiseGeneratorNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_base_in; + static const std::string k_color_input; + static const std::string k_strength_input; +}; + +} // namespace olive + +#endif // OAK_NOISEGENERATORNODE_H diff --git a/src/node/src/generator/polygon/CMakeLists.txt b/src/node/src/generator/polygon/CMakeLists.txt new file mode 100644 index 000000000..8b478b0e0 --- /dev/null +++ b/src/node/src/generator/polygon/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/polygon/polygon.h + node/generator/polygon/polygon.cpp + PARENT_SCOPE +) diff --git a/src/node/src/generator/polygon/polygon.cpp b/src/node/src/generator/polygon/polygon.cpp new file mode 100644 index 000000000..31895c4a2 --- /dev/null +++ b/src/node/src/generator/polygon/polygon.cpp @@ -0,0 +1,319 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "polygon.h" + +#include +#include + +namespace olive +{ + +const std::string PolygonGenerator::k_points_input = "points_in"; +const std::string PolygonGenerator::k_color_input = "color_in"; + +#define super GeneratorWithMerge + +PolygonGenerator::PolygonGenerator() +{ + add_input(k_points_input, NodeValue::k_bezier, Vector2D(0, 0), + InputFlags(k_input_flag_array)); + + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(1.0, 1.0, 1.0))); + + const int k_middle_x = 135; + const int k_middle_y = 45; + const int k_bottom_x = 90; + const int k_bottom_y = 120; + const int k_top_y = 135; + + // The Default Pentagon(tm) + input_array_resize(k_points_input, 5); + set_split_standard_value_on_track(k_points_input, 0, 0, 0); + set_split_standard_value_on_track(k_points_input, 1, -k_top_y, 0); + set_split_standard_value_on_track(k_points_input, 0, k_middle_x, 1); + set_split_standard_value_on_track(k_points_input, 1, -k_middle_y, 1); + set_split_standard_value_on_track(k_points_input, 0, k_bottom_x, 2); + set_split_standard_value_on_track(k_points_input, 1, k_bottom_y, 2); + set_split_standard_value_on_track(k_points_input, 0, -k_bottom_x, 3); + set_split_standard_value_on_track(k_points_input, 1, k_bottom_y, 3); + set_split_standard_value_on_track(k_points_input, 0, -k_middle_x, 4); + set_split_standard_value_on_track(k_points_input, 1, -k_middle_y, 4); + + // Initiate gizmos + poly_gizmo_ = new PathGizmo(this); + add_gizmo(poly_gizmo_); +} + +std::string PolygonGenerator::name() const +{ + return "Polygon"; +} + +std::string PolygonGenerator::id() const +{ + return "org.olivevideoeditor.Olive.polygon"; +} + +std::vector PolygonGenerator::category() const +{ + return { k_category_generator }; +} + +std::string PolygonGenerator::description() const +{ + return "Generate a 2D polygon of any amount of points."; +} + +void PolygonGenerator::retranslate() +{ + super::retranslate(); + + set_input_name(k_points_input, "Points"); + set_input_name(k_color_input, "Color"); +} + +ShaderJob PolygonGenerator::get_generate_job(const NodeValueRow &value, + const VideoParams ¶ms) const +{ + VideoParams p = params; + p.set_format(PixelFormat::u8); + auto job = Texture::job(p, GenerateJob(value)); + + // Conversion to RGB + ShaderJob rgb; + rgb.set_shader_id("rgb"); + rgb.insert("texture_in", NodeValue(NodeValue::k_texture, job, this)); + rgb.insert("color_in", value.at(k_color_input)); + + return rgb; +} + +void PolygonGenerator::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + push_mergable_job(value, + Texture::job(globals.vparams(), + get_generate_job(value, globals.vparams())), + table); +} + +void PolygonGenerator::generate_frame(FramePtr frame, + const GenerateJob &job) const +{ + // Formerly drawn with QPainter into a QImage wrapping the frame buffer + // (QImage::Format_RGBA8888_Premultiplied, filled Qt::transparent). The + // rasterizer now lives behind the facade-installed PathFillBackend (see + // node/geometry.h); the path data model below is unchanged. + unsigned char *img = reinterpret_cast(frame->data()); + const int width = frame->width(); + const int height = frame->height(); + const int linesize = frame->linesize_bytes(); + + // Clear to transparent (formerly QImage::fill(Qt::transparent)) + for (int y = 0; y < height; y++) { + std::memset(img + size_t(linesize) * y, 0, size_t(width) * 4); + } + + if (PathFillBackend fill = path_fill_backend()) { + auto points = job.get(k_points_input).to_array(); + + PainterPath path = generate_path(points, input_array_size(k_points_input)); + + double par = frame->video_params().pixel_aspect_ratio().to_double(); + double scale_x = 1.0 / frame->video_params().divider() / par; + double scale_y = 1.0 / frame->video_params().divider(); + double translate_x = frame->video_params().width() / 2 * par; + double translate_y = frame->video_params().height() / 2; + + fill(path, scale_x, scale_y, translate_x, translate_y, img, width, + height, linesize); + } else { + static bool warned = false; + if (!warned) { + fprintf(stderr, + "PolygonGenerator: no PathFillBackend installed, frame left empty\n"); + warned = true; + } + } +} + +template NodeGizmo *PolygonGenerator::create_appropriate_gizmo() +{ + return new T(this); +} + +template <> NodeGizmo *PolygonGenerator::create_appropriate_gizmo() +{ + return add_draggable_gizmo(); +} + +template +void PolygonGenerator::validate_gizmo_vector_size(std::vector &vec, int new_sz) +{ + int old_sz = int(vec.size()); + + if (old_sz != new_sz) { + if (old_sz > new_sz) { + for (int i = new_sz; i < old_sz; i++) { + delete vec.at(i); + } + } + + vec.resize(new_sz); + + if (old_sz < new_sz) { + for (int i = old_sz; i < new_sz; i++) { + vec[i] = static_cast(create_appropriate_gizmo()); + } + } + } +} + +void PolygonGenerator::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + Vector2D res; + if (TexturePtr tex = row.at(k_base_input).to_texture()) { + res = tex->virtual_resolution(); + } else { + res = globals.square_resolution(); + } + + Imath::V2d half_res(res.x() / 2, res.y() / 2); + + auto points = row.at(k_points_input).to_array(); + + int current_pos_sz = int(gizmo_position_handles_.size()); + + validate_gizmo_vector_size(gizmo_position_handles_, int(points.size())); + validate_gizmo_vector_size(gizmo_bezier_handles_, int(points.size()) * 2); + validate_gizmo_vector_size(gizmo_bezier_lines_, int(points.size()) * 2); + + for (int i = current_pos_sz; i < int(gizmo_position_handles_.size()); i++) { + gizmo_position_handles_.at(i)->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 0)); + gizmo_position_handles_.at(i)->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 1)); + + PointGizmo *bez_gizmo1 = gizmo_bezier_handles_.at(i * 2 + 0); + bez_gizmo1->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 2)); + bez_gizmo1->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 3)); + bez_gizmo1->set_shape(PointGizmo::k_circle); + bez_gizmo1->set_smaller(true); + + PointGizmo *bez_gizmo2 = gizmo_bezier_handles_.at(i * 2 + 1); + bez_gizmo2->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 4)); + bez_gizmo2->add_input( + NodeKeyframeTrackReference(NodeInput(this, k_points_input, i), 5)); + bez_gizmo2->set_shape(PointGizmo::k_circle); + bez_gizmo2->set_smaller(true); + } + + int pts_sz = input_array_size(k_points_input); + if (!points.empty()) { + for (int i = 0; i < pts_sz; i++) { + const Bezier &pt = points.at(i).to_bezier(); + + Imath::V2d main = pt.to_vec() + half_res; + Imath::V2d cp1 = main + pt.control_point_1_to_vec(); + Imath::V2d cp2 = main + pt.control_point_2_to_vec(); + + gizmo_position_handles_[i]->set_point(PointF(main.x, main.y)); + + gizmo_bezier_handles_[i * 2]->set_point(PointF(cp1.x, cp1.y)); + gizmo_bezier_lines_[i * 2]->set_line( + LineF(PointF(main.x, main.y), PointF(cp1.x, cp1.y))); + gizmo_bezier_handles_[i * 2 + 1]->set_point(PointF(cp2.x, cp2.y)); + gizmo_bezier_lines_[i * 2 + 1]->set_line( + LineF(PointF(main.x, main.y), PointF(cp2.x, cp2.y))); + } + } + + // The PathGizmo no longer stores a path (drawing data belongs to the app + // layer since the gizmo wave), so the former set_path() call with the + // half-resolution-translated generate_path() result is gone; the path + // itself is still built for generate_frame(). +} + +ShaderCode PolygonGenerator::get_shader_code(const ShaderRequest &request) const +{ + if (request.id == "rgb") { + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/rgb.frag")); + } else { + return super::get_shader_code(request); + } +} + +void PolygonGenerator::gizmo_drag_move(double x, double y, int modifiers) +{ + (void) modifiers; + + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + if (gizmo == poly_gizmo_) { + // FIXME: Drag all points + } else { + NodeInputDragger &x_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo->get_draggers()[1]; + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); + } +} + +void PolygonGenerator::add_point_to_path(PainterPath *path, const Bezier &before, + const Bezier &after) +{ + Imath::V2d a = before.to_vec() + before.control_point_2_to_vec(); + Imath::V2d b = after.to_vec() + after.control_point_1_to_vec(); + Imath::V2d c = after.to_vec(); + + path->cubic_to(PointF(a.x, a.y), PointF(b.x, b.y), PointF(c.x, c.y)); +} + +PainterPath PolygonGenerator::generate_path(const NodeValueArray &points, + int size) +{ + PainterPath path; + + if (!points.empty()) { + const Bezier &first_pt = points.at(0).to_bezier(); + Imath::V2d v = first_pt.to_vec(); + path.move_to(PointF(v.x, v.y)); + + for (int i = 1; i < size; i++) { + add_point_to_path(&path, points.at(i - 1).to_bezier(), + points.at(i).to_bezier()); + } + + add_point_to_path(&path, points.at(size - 1).to_bezier(), first_pt); + } + + return path; +} + +} diff --git a/src/node/src/generator/polygon/polygon.h b/src/node/src/generator/polygon/polygon.h new file mode 100644 index 000000000..6dcf147f7 --- /dev/null +++ b/src/node/src/generator/polygon/polygon.h @@ -0,0 +1,89 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_POLYGONGENERATOR_H +#define OAK_POLYGONGENERATOR_H + +#include "geometry.h" +#include "generator/shape/generatorwithmerge.h" +#include "gizmo/line.h" +#include "gizmo/path.h" +#include "gizmo/point.h" +#include "node.h" +#include "inputdragger.h" + +namespace olive +{ + +class PolygonGenerator : public GeneratorWithMerge { +public: + PolygonGenerator(); + + NODE_DEFAULT_FUNCTIONS(PolygonGenerator) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void generate_frame(FramePtr frame, + const GenerateJob &job) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + static const std::string k_points_input; + static const std::string k_color_input; + +protected: + ShaderJob get_generate_job(const NodeValueRow &value, + const VideoParams ¶ms) const; + + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + static void add_point_to_path(PainterPath *path, const Bezier &before, + const Bezier &after); + + static PainterPath generate_path(const NodeValueArray &points, int size); + + template + void validate_gizmo_vector_size(std::vector &vec, int new_sz); + + template NodeGizmo *create_appropriate_gizmo(); + + PathGizmo *poly_gizmo_; + std::vector gizmo_position_handles_; + std::vector gizmo_bezier_handles_; + std::vector gizmo_bezier_lines_; +}; + +} + +#endif // OAK_POLYGONGENERATOR_H diff --git a/src/node/src/generator/shape/CMakeLists.txt b/src/node/src/generator/shape/CMakeLists.txt new file mode 100644 index 000000000..78d4c34ab --- /dev/null +++ b/src/node/src/generator/shape/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/shape/generatorwithmerge.cpp + node/generator/shape/generatorwithmerge.h + node/generator/shape/shapenode.cpp + node/generator/shape/shapenode.h + node/generator/shape/shapenodebase.cpp + node/generator/shape/shapenodebase.h + PARENT_SCOPE +) diff --git a/src/node/src/generator/shape/generatorwithmerge.cpp b/src/node/src/generator/shape/generatorwithmerge.cpp new file mode 100644 index 000000000..f16b2e32f --- /dev/null +++ b/src/node/src/generator/shape/generatorwithmerge.cpp @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "generatorwithmerge.h" + +#include "math/merge/merge.h" + +namespace olive +{ + +#define super Node + +const std::string GeneratorWithMerge::k_base_input = "base_in"; + +GeneratorWithMerge::GeneratorWithMerge() +{ + add_input(k_base_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + set_effect_input(k_base_input); + set_flag(k_video_effect); +} + +void GeneratorWithMerge::retranslate() +{ + super::retranslate(); + + set_input_name(k_base_input, "Base"); +} + +ShaderCode GeneratorWithMerge::get_shader_code(const ShaderRequest &request) const +{ + if (request.id == "mrg") { + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/alphaover.frag")); + } + + return ShaderCode(); +} + +void GeneratorWithMerge::push_mergable_job(const NodeValueRow &value, + TexturePtr job, + NodeValueTable *table) const +{ + if (TexturePtr base = value.at(k_base_input).to_texture()) { + // Push as merge node + ShaderJob merge; + + merge.set_shader_id("mrg"); + merge.insert(MergeNode::k_base_in, value.at(k_base_input)); + merge.insert(MergeNode::k_blend_in, + NodeValue(NodeValue::k_texture, job, this)); + + table->push(NodeValue::k_texture, base->to_job(merge), this); + } else { + // Just push generate job + table->push(NodeValue::k_texture, job, this); + } +} + +} diff --git a/src/node/src/generator/shape/generatorwithmerge.h b/src/node/src/generator/shape/generatorwithmerge.h new file mode 100644 index 000000000..3afc2b733 --- /dev/null +++ b/src/node/src/generator/shape/generatorwithmerge.h @@ -0,0 +1,48 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_GENERATORWITHMERGE_H +#define OAK_GENERATORWITHMERGE_H + +#include "node.h" + +namespace olive +{ + +class GeneratorWithMerge : public Node { +public: + GeneratorWithMerge(); + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + static const std::string k_base_input; + +protected: + void push_mergable_job(const NodeValueRow &value, TexturePtr job, + NodeValueTable *table) const; +}; + +} + +#endif // OAK_GENERATORWITHMERGE_H diff --git a/src/node/src/generator/shape/shapenode.cpp b/src/node/src/generator/shape/shapenode.cpp new file mode 100644 index 000000000..7cb03ac5c --- /dev/null +++ b/src/node/src/generator/shape/shapenode.cpp @@ -0,0 +1,111 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "shapenode.h" + +namespace olive +{ + +#define super ShapeNodeBase + +std::string ShapeNode::k_type_input = "type_in"; +std::string ShapeNode::k_radius_input = "radius_in"; + +ShapeNode::ShapeNode() +{ + prepend_input(k_type_input, NodeValue::k_combo); + + add_input(k_radius_input, NodeValue::k_float, 20.0); + set_input_property(k_radius_input, "min", 0.0); +} + +std::string ShapeNode::name() const +{ + return "Shape"; +} + +std::string ShapeNode::id() const +{ + return "org.olivevideoeditor.Olive.shape"; +} + +std::vector ShapeNode::category() const +{ + return { k_category_generator }; +} + +std::string ShapeNode::description() const +{ + return "Generate a 2D primitive shape."; +} + +void ShapeNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_type_input, "Type"); + set_input_name(k_radius_input, "Radius"); + + // Coordinate with Type enum + set_combo_box_strings(k_type_input, + { "Rectangle", "Ellipse", "Rounded Rectangle" }); +} + +ShaderCode ShapeNode::get_shader_code(const ShaderRequest &request) const +{ + if (request.id == "shape") { + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/shape.frag")); + } else { + return super::get_shader_code(request); + } +} + +void ShapeNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + TexturePtr base = value.at(k_base_input).to_texture(); + + ShaderJob job(value); + + job.insert("resolution_in", + NodeValue(NodeValue::k_vec2, + base ? base->virtual_resolution() : + globals.square_resolution(), + this)); + job.set_shader_id("shape"); + + push_mergable_job( + value, Texture::job(base ? base->params() : globals.vparams(), job), + table); +} + +void ShapeNode::InputValueChangedEvent(const std::string &input, int element) +{ + if (input == k_type_input) { + set_input_flag(k_radius_input, k_input_flag_hidden, + (get_standard_value(k_type_input).to_int() != + k_rounded_rectangle)); + } + super::InputValueChangedEvent(input, element); +} + +} diff --git a/src/node/src/generator/shape/shapenode.h b/src/node/src/generator/shape/shapenode.h new file mode 100644 index 000000000..ebecee4dc --- /dev/null +++ b/src/node/src/generator/shape/shapenode.h @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SHAPENODE_H +#define OAK_SHAPENODE_H + +#include "shapenodebase.h" + +namespace olive +{ + +class ShapeNode : public ShapeNodeBase { +public: + ShapeNode(); + + enum Type { k_rectangle, k_ellipse, k_rounded_rectangle }; + + NODE_DEFAULT_FUNCTIONS(ShapeNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static std::string k_type_input; + static std::string k_radius_input; + +protected: + virtual void InputValueChangedEvent(const std::string &input, + int element) override; +}; + +} + +#endif // OAK_SHAPENODE_H diff --git a/src/node/src/generator/shape/shapenodebase.cpp b/src/node/src/generator/shape/shapenodebase.cpp new file mode 100644 index 000000000..d6393098e --- /dev/null +++ b/src/node/src/generator/shape/shapenodebase.cpp @@ -0,0 +1,374 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "shapenodebase.h" + +#include + +#include "nodeundo.h" + +namespace olive +{ + +#define super GeneratorWithMerge + +// Qt::AltModifier / Qt::ShiftModifier flag values (modifiers arrive as an int) +static const int k_alt_modifier = 0x08000000; +static const int k_shift_modifier = 0x02000000; + +const std::string ShapeNodeBase::k_position_input = "pos_in"; +const std::string ShapeNodeBase::k_size_input = "size_in"; +const std::string ShapeNodeBase::k_color_input = "color_in"; + +ShapeNodeBase::ShapeNodeBase(bool create_color_input) +{ + add_input(k_position_input, NodeValue::k_vec2, Vector2D(0, 0)); + add_input(k_size_input, NodeValue::k_vec2, Vector2D(100, 100)); + set_input_property(k_size_input, "min", Vector2D(0, 0)); + + if (create_color_input) { + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(1.0, 0.0, 0.0, 1.0))); + } + + // Initiate gizmos + std::vector pos_n_sz = { + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1), + NodeKeyframeTrackReference(NodeInput(this, k_size_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_size_input), 1) + }; + poly_gizmo_ = add_draggable_gizmo({ + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 0), + NodeKeyframeTrackReference(NodeInput(this, k_position_input), 1), + }); + for (int i = 0; i < k_gizmo_scale_count; i++) { + point_gizmo_[i] = + add_draggable_gizmo(pos_n_sz, PointGizmo::k_absolute); + } +} + +void ShapeNodeBase::retranslate() +{ + super::retranslate(); + + set_input_name(k_position_input, "Position"); + set_input_name(k_size_input, "Size"); + + if (has_input_with_id(k_color_input)) { + set_input_name(k_color_input, "Color"); + } +} + +void ShapeNodeBase::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + // Use offsets to make the appearance of values that start in the top left, even though we + // really anchor around the center + Vector2D center_pt = globals.square_resolution() * 0.5f; + set_input_property(k_position_input, "offset", center_pt); + + Vector2D pos = row.at(k_position_input).to_vec2(); + Vector2D sz = row.at(k_size_input).to_vec2(); + Vector2D half_sz = sz * 0.5f; + + double left_pt = pos.x() + center_pt.x() - half_sz.x(); + double top_pt = pos.y() + center_pt.y() - half_sz.y(); + double right_pt = left_pt + sz.x(); + double bottom_pt = top_pt + sz.y(); + double center_x_pt = (left_pt + right_pt) * 0.5; + double center_y_pt = (top_pt + bottom_pt) * 0.5; + + point_gizmo_[k_gizmo_scale_top_left]->set_point(PointF(left_pt, top_pt)); + point_gizmo_[k_gizmo_scale_top_center]->set_point(PointF(center_x_pt, top_pt)); + point_gizmo_[k_gizmo_scale_top_right]->set_point(PointF(right_pt, top_pt)); + point_gizmo_[k_gizmo_scale_bottom_left]->set_point(PointF(left_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_bottom_center]->set_point( + PointF(center_x_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_bottom_right]->set_point( + PointF(right_pt, bottom_pt)); + point_gizmo_[k_gizmo_scale_center_left]->set_point( + PointF(left_pt, center_y_pt)); + point_gizmo_[k_gizmo_scale_center_right]->set_point( + PointF(right_pt, center_y_pt)); + + // QPolygonF is now std::vector; the corners below mirror the + // QPolygonF(QRectF) constructor order (top left, top right, bottom right, + // bottom left) + poly_gizmo_->set_polygon({ PointF(left_pt, top_pt), PointF(right_pt, top_pt), + PointF(right_pt, bottom_pt), + PointF(left_pt, bottom_pt) }); +} + +void ShapeNodeBase::set_rect(RectF rect, const VideoParams &sequence_res, + MultiUndoCommand *command) +{ + // Normalize around center of sequence (RectF is a plain data carrier now, + // so the two translate() calls are spelled out) + rect = RectF(rect.x() - sequence_res.width() * 0.5, + rect.y() - sequence_res.height() * 0.5, rect.width(), + rect.height()); + rect = RectF(rect.x() + rect.width() * 0.5, + rect.y() + rect.height() * 0.5, rect.width(), rect.height()); + + NodeInput pos(this, ShapeNodeBase::k_position_input); + NodeInput sz(this, ShapeNodeBase::k_size_input); + + command->add_child(new NodeParamSetStandardValueCommand( + NodeKeyframeTrackReference(sz, 0), rect.width())); + command->add_child(new NodeParamSetStandardValueCommand( + NodeKeyframeTrackReference(sz, 1), rect.height())); + command->add_child(new NodeParamSetStandardValueCommand( + NodeKeyframeTrackReference(pos, 0), rect.x())); + command->add_child(new NodeParamSetStandardValueCommand( + NodeKeyframeTrackReference(pos, 1), rect.y())); +} + +void ShapeNodeBase::gizmo_drag_move(double x, double y, int modifiers) +{ + DraggableGizmo *gizmo = static_cast(current_gizmo()); + + NodeInputDragger &x_drag = gizmo->get_draggers()[0]; + NodeInputDragger &y_drag = gizmo->get_draggers()[1]; + + if (gizmo == poly_gizmo_) { + x_drag.drag(x_drag.get_start_value().to_double() + x); + y_drag.drag(y_drag.get_start_value().to_double() + y); + } else { + bool from_center = modifiers & k_alt_modifier; + bool keep_ratio = modifiers & k_shift_modifier; + + NodeInputDragger &w_drag = gizmo->get_draggers()[2]; + NodeInputDragger &h_drag = gizmo->get_draggers()[3]; + + Vector2D gizmo_sz_start(w_drag.get_start_value().to_double(), + h_drag.get_start_value().to_double()); + Vector2D gizmo_pos_start(x_drag.get_start_value().to_double(), + y_drag.get_start_value().to_double()); + Vector2D gizmo_half_res = gizmo->get_globals().square_resolution() / 2; + Vector2D adjusted_pt(x, y); + Vector2D new_size; + Vector2D new_pos; + Vector2D anchor; + static const int k_xy_count = 2; + bool negative[k_xy_count] = { false }; + + double original_ratio; + if (keep_ratio) { + original_ratio = w_drag.get_start_value().to_double() / + h_drag.get_start_value().to_double(); + } + + // Calculate new size + if (from_center) { + // Calculate new size by using distance from center and doubling it + new_size = (adjusted_pt - gizmo_half_res - gizmo_pos_start) * 2; + + if (is_gizmo_top(gizmo)) { + new_size.set_y(-new_size.y()); + } + + if (is_gizmo_left(gizmo)) { + new_size.set_x(-new_size.x()); + } + } else { + // Calculate new size by using distance from "anchor" - i.e. the opposite point of the shape + // from the gizmo being dragged + adjusted_pt -= gizmo_half_res; + + anchor = generate_gizmo_anchor(gizmo_pos_start, gizmo_sz_start, gizmo, + &adjusted_pt) + + gizmo_half_res; + + adjusted_pt += gizmo_half_res; + + // Calculate size and position + new_size = adjusted_pt - anchor; + + // Abs size so neither coord is negative (component-wise, formerly + // a loop over QVector2D::operator[]) + if (new_size.x() < 0) { + negative[0] = true; + new_size.set_x(-new_size.x()); + } + if (new_size.y() < 0) { + negative[1] = true; + new_size.set_y(-new_size.y()); + } + } + + // Restrict sizes by constraints + if (is_gizmo_vertical_center(gizmo)) { + if (keep_ratio) { + // Calculate width from new height + new_size.set_x(new_size.y() * original_ratio); + } else { + // Constrain to original width + new_size.set_x(gizmo_sz_start.x()); + } + } + + if (is_gizmo_horizontal_center(gizmo)) { + if (keep_ratio) { + // Calculate height from new width + new_size.set_y(new_size.x() * original_ratio); + } else { + // Constrain to original height + new_size.set_y(gizmo_sz_start.y()); + } + } + + if (is_gizmo_corner(gizmo)) { + if (keep_ratio) { + float hypot = std::hypot(new_size.x(), new_size.y()); + + float original_angle = + std::atan2(gizmo_sz_start.x(), gizmo_sz_start.y()); + + // Calculate new size based on original angle and hypotenuse + new_size.set_x(std::sin(original_angle) * hypot); + new_size.set_y(std::cos(original_angle) * hypot); + } + } + + // Calculate position + if (from_center) { + new_pos = gizmo_pos_start; + } else { + Vector2D using_size = new_size; + + // Un-abs size (component-wise, see above) + if (negative[0]) { + using_size.set_x(-using_size.x()); + } + if (negative[1]) { + using_size.set_y(-using_size.y()); + } + + // I'm pretty sure there's an algorithmic way of doing this, but I'm tired and this works + if (is_gizmo_horizontal_center(gizmo)) { + using_size.set_y(0); + } + + if (is_gizmo_vertical_center(gizmo)) { + using_size.set_x(0); + } + + new_pos = + generate_gizmo_anchor(gizmo_pos_start, gizmo_sz_start, gizmo) + + using_size / 2; + } + + x_drag.drag(new_pos.x()); + y_drag.drag(new_pos.y()); + w_drag.drag(new_size.x()); + h_drag.drag(new_size.y()); + } +} + +Vector2D ShapeNodeBase::generate_gizmo_anchor(const Vector2D &pos, + const Vector2D &size, + NodeGizmo *gizmo, + Vector2D *pt) const +{ + Vector2D anchor = pos; + Vector2D half_sz = size / 2; + + if (is_gizmo_left(gizmo)) { + anchor.set_x(anchor.x() + half_sz.x()); + if (pt && pt->x() > anchor.x()) { + pt->set_x(anchor.x()); + } + } + + if (is_gizmo_right(gizmo)) { + anchor.set_x(anchor.x() - half_sz.x()); + if (pt && pt->x() < anchor.x()) { + pt->set_x(anchor.x()); + } + } + + if (is_gizmo_top(gizmo)) { + anchor.set_y(anchor.y() + half_sz.y()); + if (pt && pt->y() > anchor.y()) { + pt->set_y(anchor.y()); + } + } + + if (is_gizmo_bottom(gizmo)) { + anchor.set_y(anchor.y() - half_sz.y()); + if (pt && pt->y() < anchor.y()) { + pt->set_y(anchor.y()); + } + } + + return anchor; +} + +bool ShapeNodeBase::is_gizmo_top(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_top_center] || + g == point_gizmo_[k_gizmo_scale_top_left] || + g == point_gizmo_[k_gizmo_scale_top_right]; +} + +bool ShapeNodeBase::is_gizmo_bottom(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_bottom_center] || + g == point_gizmo_[k_gizmo_scale_bottom_left] || + g == point_gizmo_[k_gizmo_scale_bottom_right]; +} + +bool ShapeNodeBase::is_gizmo_left(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_top_left] || + g == point_gizmo_[k_gizmo_scale_center_left] || + g == point_gizmo_[k_gizmo_scale_bottom_left]; +} + +bool ShapeNodeBase::is_gizmo_right(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_top_right] || + g == point_gizmo_[k_gizmo_scale_center_right] || + g == point_gizmo_[k_gizmo_scale_bottom_right]; +} + +bool ShapeNodeBase::is_gizmo_horizontal_center(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_center_left] || + g == point_gizmo_[k_gizmo_scale_center_right]; +} + +bool ShapeNodeBase::is_gizmo_vertical_center(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_top_center] || + g == point_gizmo_[k_gizmo_scale_bottom_center]; +} + +bool ShapeNodeBase::is_gizmo_corner(NodeGizmo *g) const +{ + return g == point_gizmo_[k_gizmo_scale_top_left] || + g == point_gizmo_[k_gizmo_scale_top_right] || + g == point_gizmo_[k_gizmo_scale_bottom_right] || + g == point_gizmo_[k_gizmo_scale_bottom_left]; +} + +} diff --git a/src/node/src/generator/shape/shapenodebase.h b/src/node/src/generator/shape/shapenodebase.h new file mode 100644 index 000000000..b897dfc8e --- /dev/null +++ b/src/node/src/generator/shape/shapenodebase.h @@ -0,0 +1,81 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SHAPENODEBASE_H +#define OAK_SHAPENODEBASE_H + +#include "generatorwithmerge.h" +#include "geometry.h" +#include "gizmo/point.h" +#include "gizmo/polygon.h" +#include "gizmo/text.h" +#include "inputdragger.h" +#include "node.h" + +namespace olive +{ + +class ShapeNodeBase : public GeneratorWithMerge { +public: + ShapeNodeBase(bool create_color_input = true); + + virtual void retranslate() override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + void set_rect(RectF rect, const VideoParams &sequence_res, + MultiUndoCommand *command); + + static const std::string k_position_input; + static const std::string k_size_input; + static const std::string k_color_input; + +protected: + PolygonGizmo *poly_gizmo() const + { + return poly_gizmo_; + } + + virtual void gizmo_drag_move(double x, double y, int modifiers) override; + +private: + Vector2D generate_gizmo_anchor(const Vector2D &pos, const Vector2D &size, + NodeGizmo *gizmo, + Vector2D *pt = nullptr) const; + + bool is_gizmo_top(NodeGizmo *g) const; + bool is_gizmo_bottom(NodeGizmo *g) const; + bool is_gizmo_left(NodeGizmo *g) const; + bool is_gizmo_right(NodeGizmo *g) const; + bool is_gizmo_horizontal_center(NodeGizmo *g) const; + bool is_gizmo_vertical_center(NodeGizmo *g) const; + bool is_gizmo_corner(NodeGizmo *g) const; + + // Gizmo variables + static const int k_gizmo_whole_rect = k_gizmo_scale_count; + PointGizmo *point_gizmo_[k_gizmo_scale_count]; + PolygonGizmo *poly_gizmo_; +}; + +} + +#endif // OAK_SHAPENODEBASE_H diff --git a/src/node/src/generator/solid/CMakeLists.txt b/src/node/src/generator/solid/CMakeLists.txt new file mode 100644 index 000000000..342e9c824 --- /dev/null +++ b/src/node/src/generator/solid/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/solid/solid.h + node/generator/solid/solid.cpp + PARENT_SCOPE +) diff --git a/src/node/src/generator/solid/solid.cpp b/src/node/src/generator/solid/solid.cpp new file mode 100644 index 000000000..601102ecb --- /dev/null +++ b/src/node/src/generator/solid/solid.cpp @@ -0,0 +1,80 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "solid.h" + +namespace olive +{ + +const std::string SolidGenerator::k_color_input = "color_in"; + +#define super Node + +SolidGenerator::SolidGenerator() +{ + // Default to a color that isn't black + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(1.0f, 0.0f, 0.0f, 1.0f))); +} + +std::string SolidGenerator::name() const +{ + return "Solid"; +} + +std::string SolidGenerator::id() const +{ + return "org.olivevideoeditor.Olive.solidgenerator"; +} + +std::vector SolidGenerator::category() const +{ + return { k_category_generator }; +} + +std::string SolidGenerator::description() const +{ + return "Generate a solid color."; +} + +void SolidGenerator::retranslate() +{ + super::retranslate(); + + set_input_name(k_color_input, "Color"); +} + +void SolidGenerator::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + table->push(NodeValue::k_texture, + Texture::job(globals.vparams(), ShaderJob(value)), this); +} + +ShaderCode SolidGenerator::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode(FileFunctions::read_file_as_string(":/shaders/solid.frag")); +} + +} diff --git a/src/node/src/generator/solid/solid.h b/src/node/src/generator/solid/solid.h new file mode 100644 index 000000000..27d2fa503 --- /dev/null +++ b/src/node/src/generator/solid/solid.h @@ -0,0 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SOLIDGENERATOR_H +#define OAK_SOLIDGENERATOR_H + +#include "node.h" + +namespace olive +{ + +class SolidGenerator : public Node { +public: + SolidGenerator(); + + NODE_DEFAULT_FUNCTIONS(SolidGenerator) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + static const std::string k_color_input; +}; + +} + +#endif // OAK_SOLIDGENERATOR_H diff --git a/src/node/src/generator/text/CMakeLists.txt b/src/node/src/generator/text/CMakeLists.txt new file mode 100644 index 000000000..516b545fe --- /dev/null +++ b/src/node/src/generator/text/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/generator/text/textv1.cpp + node/generator/text/textv1.h + node/generator/text/textv2.cpp + node/generator/text/textv2.h + node/generator/text/textv3.cpp + node/generator/text/textv3.h + PARENT_SCOPE +) diff --git a/src/node/src/generator/text/textbackend.h b/src/node/src/generator/text/textbackend.h new file mode 100644 index 000000000..a97abb843 --- /dev/null +++ b/src/node/src/generator/text/textbackend.h @@ -0,0 +1,140 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TEXTBACKEND_H +#define OAK_TEXTBACKEND_H + +#include + +namespace olive +{ + +/** + * @brief POD description of a text layout/rasterization job. + * + * Replaces the QTextDocument/QTextOption/QAbstractTextDocumentLayout usage + * in the text generator nodes. oaknode carries only the data; the actual + * text engine (formerly Qt's rich text layout + QPainter rasterizer) is + * installed by the facade/app layer through the backend hooks at the bottom + * of this header. + */ +struct TextLayoutRequest { + enum Mode { + k_plain_text, // QTextDocument::setPlainText() + k_html, // QTextDocument::setHtml() + k_olive_html // Olive's Html::html_to_doc() rich text + }; + + std::string text; // plain text or markup, per mode + Mode mode = k_plain_text; + std::string font_family; // default font family (empty = backend default) + double font_size_pt = 0.0; // default font size in points (0 = default) + int dots_per_meter = 0; // paint device resolution (0 = backend default) + double wrap_width = 0.0; // QTextDocument::setTextWidth() + bool center_horizontally = false; // default QTextOption(Qt::AlignCenter) + + // The backends always paint with white as the default text color + // (formerly PaintContext palette QPalette::Text = Qt::white); HTML + // markup may override it per span. +}; + +/** + * @brief Laid-out document size (formerly QTextDocument::size()). + */ +struct TextLayoutSize { + double width = 0.0; + double height = 0.0; +}; + +/** + * @brief Target pixel buffer for text rasterization. + * + * channel_count 1 = 8-bit grayscale coverage (formerly + * QImage::Format_Grayscale8, the node tints it afterwards); + * channel_count 4 = 8-bit RGBA premultiplied drawn over the existing + * (cleared) buffer content (formerly QImage::Format_RGBA8888_Premultiplied). + */ +struct TextRenderTarget { + unsigned char *data; + int width; + int height; + int linesize_bytes; + int channel_count; +}; + +/** + * @brief Draw transform, mirroring the original QPainter calls. + * + * The painter first applied `scale(scale, scale)`, then translated to the + * base offset, set the clip rect (0, 0, clip_width, clip_height) at that + * base offset, and finally translated by the remaining draw offset before + * drawing. A point p of the laid-out document therefore lands at + * ((p.x + draw_offset_x) * scale, (p.y + draw_offset_y) * scale), clipped + * to the rectangle ((clip_offset_*) * scale, (clip_size) * scale) relative + * to the same origin. clip_enabled=false means no clip. + */ +struct TextRenderTransform { + double scale = 1.0; + double draw_offset_x = 0.0; + double draw_offset_y = 0.0; + bool clip_enabled = false; + double clip_offset_x = 0.0; + double clip_offset_y = 0.0; + double clip_width = 0.0; + double clip_height = 0.0; +}; + +/** + * @brief Backend hooks installed by the facade/app layer. + * + * measure lays out the request and returns the document size (formerly + * QTextDocument::size()). render draws the request into the target buffer + * (formerly QAbstractTextDocumentLayout::draw()). When a hook is null the + * caller falls back to a zero size / leaves the (already cleared) buffer + * untouched — a documented behavior gap until the facade installs a text + * engine. + */ +using TextMeasureBackend = TextLayoutSize (*)(const TextLayoutRequest &request); +using TextRenderBackend = void (*)(const TextLayoutRequest &request, + const TextRenderTransform &transform, + const TextRenderTarget &target); + +inline TextMeasureBackend g_text_measure_backend = nullptr; +inline TextRenderBackend g_text_render_backend = nullptr; + +inline void set_text_backends(TextMeasureBackend measure, TextRenderBackend render) +{ + g_text_measure_backend = measure; + g_text_render_backend = render; +} + +inline TextMeasureBackend text_measure_backend() +{ + return g_text_measure_backend; +} + +inline TextRenderBackend text_render_backend() +{ + return g_text_render_backend; +} + +} + +#endif // OAK_TEXTBACKEND_H diff --git a/src/node/src/generator/text/textv1.cpp b/src/node/src/generator/text/textv1.cpp new file mode 100644 index 000000000..c6f041a5e --- /dev/null +++ b/src/node/src/generator/text/textv1.cpp @@ -0,0 +1,206 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "textv1.h" + +#include +#include + +#include "textbackend.h" + +namespace olive +{ + +enum TextVerticalAlign { + k_vertical_align_top, + k_vertical_align_center, + k_vertical_align_bottom, +}; + +const std::string TextGeneratorV1::k_text_input = "text_in"; +const std::string TextGeneratorV1::k_html_input = "html_in"; +const std::string TextGeneratorV1::k_color_input = "color_in"; +const std::string TextGeneratorV1::k_v_align_input = "valign_in"; +const std::string TextGeneratorV1::k_font_input = "font_in"; +const std::string TextGeneratorV1::k_font_size_input = "font_size_in"; + +#define super Node + +TextGeneratorV1::TextGeneratorV1() +{ + add_input(k_text_input, NodeValue::k_text, "Sample Text"); + + add_input(k_html_input, NodeValue::k_boolean, false); + + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(1.0f, 1.0f, 1.0))); + + add_input(k_v_align_input, NodeValue::k_combo, 1); + + add_input(k_font_input, NodeValue::k_font); + + add_input(k_font_size_input, NodeValue::k_float, 72.0f); + + set_flag(k_dont_show_in_create_menu); +} + +std::string TextGeneratorV1::name() const +{ + return "Text (Legacy)"; +} + +std::string TextGeneratorV1::id() const +{ + return "org.olivevideoeditor.Olive.textgenerator"; +} + +std::vector TextGeneratorV1::category() const +{ + return { k_category_generator }; +} + +std::string TextGeneratorV1::description() const +{ + return "Generate rich text."; +} + +void TextGeneratorV1::retranslate() +{ + super::retranslate(); + + set_input_name(k_text_input, "Text"); + set_input_name(k_html_input, "Enable HTML"); + set_input_name(k_font_input, "Font"); + set_input_name(k_font_size_input, "Font Size"); + set_input_name(k_color_input, "Color"); + set_input_name(k_v_align_input, "Vertical Align"); + set_combo_box_strings(k_v_align_input, { "Top", "Center", "Bottom" }); +} + +void TextGeneratorV1::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (!value.at(k_text_input).to_string().empty()) { + table->push(NodeValue::k_texture, + Texture::job(globals.vparams(), GenerateJob(value)), this); + } +} + +void TextGeneratorV1::generate_frame(FramePtr frame, + const GenerateJob &job) const +{ + // Formerly laid out and rasterized with QTextDocument + QPainter into a + // grayscale QImage (the alpha channel was then transplanted to the float + // buffer with the correct RGB). Layout/rasterization now runs behind the + // facade-installed text backend (see textbackend.h); the coverage buffer + // handling and the transplant loop are unchanged. + const int width = frame->width(); + const int height = frame->height(); + + // QImage::Format_Grayscale8 scanlines are 32-bit aligned + const int linesize = (width + 3) & ~3; + std::vector img(size_t(linesize) * height, 0); + + TextLayoutRequest req; + req.text = job.get(k_text_input).to_string(); + req.font_family = job.get(k_font_input).to_string(); + req.font_size_pt = + job.get(k_font_size_input).to_double(); + // Center by default (formerly QTextOption(Qt::AlignCenter)) + req.center_horizontally = true; + if (job.get(k_html_input).to_bool()) { + req.mode = TextLayoutRequest::k_html; + // QTextDocument::setHtml() doesn't translate newlines, so they were + // replaced with
tags first + std::string::size_type nl = 0; + while ((nl = req.text.find('\n', nl)) != std::string::npos) { + req.text.replace(nl, 1, "
"); + nl += 4; + } + } + + // Align to 80% width because that's considered the "title safe" area + int tenth_of_width = frame->video_params().width() / 10; + req.wrap_width = tenth_of_width * 8; + + TextLayoutSize doc; + if (TextMeasureBackend measure = text_measure_backend()) { + doc = measure(req); + } + + double scale = 1.0 / frame->video_params().divider(); + + // Push 10% inwards to compensate for title safe area + double offset_x = tenth_of_width; + double offset_y = 0; + + TextVerticalAlign valign = static_cast( + job.get(k_v_align_input).to_int()); + int doc_height = int(doc.height); + + switch (valign) { + case k_vertical_align_top: + // Push 10% inwards for title safe area + offset_y = frame->video_params().height() / 10; + break; + case k_vertical_align_center: + // Center align + offset_y = frame->video_params().height() / 2 - doc_height / 2; + break; + case k_vertical_align_bottom: + // Push 10% inwards for title safe area + offset_y = frame->video_params().height() - doc_height - + frame->video_params().height() / 10; + break; + } + + if (TextRenderBackend render = text_render_backend()) { + TextRenderTransform t; + t.scale = scale; + t.draw_offset_x = offset_x; + t.draw_offset_y = offset_y; + TextRenderTarget target = { img.data(), width, height, linesize, 1 }; + render(req, t, target); + } else { + static bool warned = false; + if (!warned) { + fprintf(stderr, + "TextGeneratorV1: no text backend installed, frame left empty\n"); + warned = true; + } + } + + // Transplant alpha channel to frame + Color rgb = job.get(k_color_input).to_color(); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + unsigned char src_alpha = img[size_t(linesize) * y + x]; + float alpha = float(src_alpha) / 255.0f; + + frame->set_pixel(x, y, + Color(rgb.red() * alpha, rgb.green() * alpha, + rgb.blue() * alpha, alpha)); + } + } +} + +} diff --git a/src/node/src/generator/text/textv1.h b/src/node/src/generator/text/textv1.h new file mode 100644 index 000000000..8661ce534 --- /dev/null +++ b/src/node/src/generator/text/textv1.h @@ -0,0 +1,59 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TEXTGENERATORV1_H +#define OAK_TEXTGENERATORV1_H + +#include "node.h" + +namespace olive +{ + +class TextGeneratorV1 : public Node { +public: + TextGeneratorV1(); + + NODE_DEFAULT_FUNCTIONS(TextGeneratorV1) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void generate_frame(FramePtr frame, + const GenerateJob &job) const override; + + static const std::string k_text_input; + static const std::string k_html_input; + static const std::string k_color_input; + static const std::string k_v_align_input; + static const std::string k_font_input; + static const std::string k_font_size_input; +}; + +} + +#endif // OAK_TEXTGENERATORV1_H diff --git a/src/node/src/generator/text/textv2.cpp b/src/node/src/generator/text/textv2.cpp new file mode 100644 index 000000000..2b861dffd --- /dev/null +++ b/src/node/src/generator/text/textv2.cpp @@ -0,0 +1,231 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "textv2.h" + +#include +#include + +#include + +#include "textbackend.h" + +namespace olive +{ + +#define super ShapeNodeBase + +enum TextVerticalAlign { + k_vertical_align_top, + k_vertical_align_center, + k_vertical_align_bottom, +}; + +const std::string TextGeneratorV2::k_text_input = "text_in"; +const std::string TextGeneratorV2::k_html_input = "html_in"; +const std::string TextGeneratorV2::k_v_align_input = "valign_in"; +const std::string TextGeneratorV2::k_font_input = "font_in"; +const std::string TextGeneratorV2::k_font_size_input = "font_size_in"; + +TextGeneratorV2::TextGeneratorV2() +{ + add_input(k_text_input, NodeValue::k_text, "Sample Text"); + + add_input(k_html_input, NodeValue::k_boolean, false); + + add_input(k_v_align_input, NodeValue::k_combo, k_vertical_align_top); + + add_input(k_font_input, NodeValue::k_font); + + add_input(k_font_size_input, NodeValue::k_float, 72.0f); + + set_standard_value(k_color_input, Variant::from_value(Color(1.0f, 1.0f, 1.0))); + set_standard_value(k_size_input, Vector2D(400, 300)); + + set_flag(k_dont_show_in_create_menu); +} + +std::string TextGeneratorV2::name() const +{ + return "Text (Legacy)"; +} + +std::string TextGeneratorV2::id() const +{ + return "org.olivevideoeditor.Olive.text2"; +} + +std::vector TextGeneratorV2::category() const +{ + return { k_category_generator }; +} + +std::string TextGeneratorV2::description() const +{ + return "Generate rich text."; +} + +void TextGeneratorV2::retranslate() +{ + super::retranslate(); + + set_input_name(k_text_input, "Text"); + set_input_name(k_html_input, "Enable HTML"); + set_input_name(k_font_input, "Font"); + set_input_name(k_font_size_input, "Font Size"); + set_input_name(k_v_align_input, "Vertical Align"); + set_combo_box_strings(k_v_align_input, { "Top", "Center", "Bottom" }); +} + +void TextGeneratorV2::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (!value.at(k_text_input).to_string().empty()) { + GenerateJob job(value); + auto text_params = globals.vparams(); + text_params.set_format(PixelFormat::f32); + table->push(NodeValue::k_texture, Texture::job(text_params, job), this); + } +} + +void TextGeneratorV2::generate_frame(FramePtr frame, + const GenerateJob &job) const +{ + // Formerly laid out and rasterized with QTextDocument + QPainter into a + // grayscale QImage (the alpha channel was then transplanted to the float + // buffer with the correct RGB). Layout/rasterization now runs behind the + // facade-installed text backend (see textbackend.h); the coverage buffer + // handling and the transplant loop are unchanged. + const int width = frame->width(); + const int height = frame->height(); + + // QImage::Format_Grayscale8 scanlines are 32-bit aligned + const int linesize = (width + 3) & ~3; + std::vector img(size_t(linesize) * height, 0); + + TextLayoutRequest req; + req.text = job.get(k_text_input).to_string(); + req.font_family = job.get(k_font_input).to_string(); + req.font_size_pt = + job.get(k_font_size_input).to_double(); + // 72 DPI in DPM (72 / 2.54 * 100) + req.dots_per_meter = 2835; + if (job.get(k_html_input).to_bool()) { + req.mode = TextLayoutRequest::k_html; + // QTextDocument::setHtml() doesn't translate newlines, so they were + // replaced with
tags first + std::string::size_type nl = 0; + while ((nl = req.text.find('\n', nl)) != std::string::npos) { + req.text.replace(nl, 1, "
"); + nl += 4; + } + } + + Vector2D size = job.get(k_size_input).to_vec2(); + req.wrap_width = size.x(); + + TextLayoutSize doc; + if (TextMeasureBackend measure = text_measure_backend()) { + doc = measure(req); + } + + double scale = 1.0 / frame->video_params().divider(); + + Vector2D pos = job.get(k_position_input).to_vec2(); + double base_offset_x = pos.x() - size.x() / 2 + + frame->video_params().width() / 2; + double base_offset_y = pos.y() - size.y() / 2 + + frame->video_params().height() / 2; + + TextVerticalAlign valign = static_cast( + job.get(k_v_align_input).to_int()); + int doc_height = int(doc.height); + + double draw_offset_x = base_offset_x; + double draw_offset_y = base_offset_y; + + switch (valign) { + case k_vertical_align_top: + // Do nothing + break; + case k_vertical_align_center: + // Center align + draw_offset_y += size.y() / 2 - doc_height / 2; + break; + case k_vertical_align_bottom: + draw_offset_y += size.y() - doc_height; + break; + } + + if (TextRenderBackend render = text_render_backend()) { + TextRenderTransform t; + t.scale = scale; + t.draw_offset_x = draw_offset_x; + t.draw_offset_y = draw_offset_y; + // The clip rect was set before the vertical alignment translate + t.clip_enabled = true; + t.clip_offset_x = base_offset_x; + t.clip_offset_y = base_offset_y; + t.clip_width = size.x(); + t.clip_height = size.y(); + TextRenderTarget target = { img.data(), width, height, linesize, 1 }; + render(req, t, target); + } else { + static bool warned = false; + if (!warned) { + fprintf(stderr, + "TextGeneratorV2: no text backend installed, frame left empty\n"); + warned = true; + } + } + + // Transplant alpha channel to frame + Color rgba = job.get(k_color_input).to_color(); +#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM) + __m128 sse_color = _mm_loadu_ps(rgba.data()); +#endif + + float *frame_dst = reinterpret_cast(frame->data()); + for (int y = 0; y < height; y++) { + const unsigned char *src_y = img.data() + size_t(linesize) * y; + float *dst_y = frame_dst + y * frame->linesize_pixels() * + VideoParams::k_rgba_channel_count; + + for (int x = 0; x < width; x++) { + float alpha = float(src_y[x]) / 255.0f; + float *dst = dst_y + x * VideoParams::k_rgba_channel_count; + +#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM) + __m128 sse_alpha = _mm_load1_ps(&alpha); + __m128 sse_res = _mm_mul_ps(sse_color, sse_alpha); + + _mm_store_ps(dst, sse_res); +#else + for (int i = 0; i < VideoParams::k_rgba_channel_count; i++) { + dst[i] = rgba.data()[i] * alpha; + } +#endif + } + } +} + +} diff --git a/src/node/src/generator/text/textv2.h b/src/node/src/generator/text/textv2.h new file mode 100644 index 000000000..51618795f --- /dev/null +++ b/src/node/src/generator/text/textv2.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TEXTGENERATORV2_H +#define OAK_TEXTGENERATORV2_H + +#include "generator/shape/shapenodebase.h" + +namespace olive +{ + +class TextGeneratorV2 : public ShapeNodeBase { +public: + TextGeneratorV2(); + + NODE_DEFAULT_FUNCTIONS(TextGeneratorV2) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void generate_frame(FramePtr frame, + const GenerateJob &job) const override; + + static const std::string k_text_input; + static const std::string k_html_input; + static const std::string k_v_align_input; + static const std::string k_font_input; + static const std::string k_font_size_input; +}; + +} + +#endif // OAK_TEXTGENERATORV2_H diff --git a/src/node/src/generator/text/textv3.cpp b/src/node/src/generator/text/textv3.cpp new file mode 100644 index 000000000..09af108f4 --- /dev/null +++ b/src/node/src/generator/text/textv3.cpp @@ -0,0 +1,360 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "textv3.h" + +#include +#include +#include +#include + +#include "coreengine.h" +#include "project.h" +#include "nodeundo.h" +#include "textbackend.h" + +namespace olive +{ + +#define super ShapeNodeBase + +enum TextVerticalAlign { + k_vertical_align_top, + k_vertical_align_center, + k_vertical_align_bottom, +}; + +const std::string TextGeneratorV3::k_text_input = "text_in"; +const std::string TextGeneratorV3::k_vertical_alignment_input = "valign_in"; +const std::string TextGeneratorV3::k_use_args_input = "use_args_in"; +const std::string TextGeneratorV3::k_args_input = "args_in"; + +TextGeneratorV3::TextGeneratorV3() + : ShapeNodeBase(false) + , dont_emit_valign_(false) +{ + add_input(k_text_input, NodeValue::k_text, + std::string("

") + + "Sample Text" + "

"); + set_input_property(k_text_input, "vieweronly", true); + + set_standard_value(k_size_input, Vector2D(400, 300)); + + add_input(k_vertical_alignment_input, NodeValue::k_combo, + InputFlags(k_input_flag_hidden | k_input_flag_static)); + + add_input(k_use_args_input, NodeValue::k_boolean, true, + InputFlags(k_input_flag_hidden | k_input_flag_static)); + + add_input(k_args_input, NodeValue::k_text, InputFlags(k_input_flag_array)); + set_input_property(k_args_input, "arraystart", 1); + + text_gizmo_ = new TextGizmo(this); + add_gizmo(text_gizmo_); + text_gizmo_->set_input(NodeInput(this, k_text_input)); + // The TextGizmo activated/deactivated signal connections were removed + // with QObject; the gizmo wave / facade invokes gizmo_activated() and + // gizmo_deactivated() directly. +} + +std::string TextGeneratorV3::name() const +{ + return "Text"; +} + +std::string TextGeneratorV3::id() const +{ + return "org.olivevideoeditor.Olive.text3"; +} + +std::vector TextGeneratorV3::category() const +{ + return { k_category_generator }; +} + +std::string TextGeneratorV3::description() const +{ + return "Generate rich text."; +} + +void TextGeneratorV3::retranslate() +{ + super::retranslate(); + + set_input_name(k_text_input, "Text"); + set_input_name(k_vertical_alignment_input, "Vertical Alignment"); + set_combo_box_strings(k_vertical_alignment_input, + { "Top", "Middle", "Bottom" }); + set_input_name(k_args_input, "Arguments"); +} + +void TextGeneratorV3::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + std::string text = value.at(k_text_input).to_string(); + + if (value.at(k_use_args_input).to_bool()) { + auto args = value.at(k_args_input).to_array(); + if (!args.empty()) { + StringList list; + list.reserve(args.size()); + for (size_t i = 0; i < args.size(); i++) { + list.push_back(args[int(i)].to_string()); + } + + text = format_string(text, list); + } + } + + if (!text.empty()) { + TexturePtr base = value.at(k_text_input).to_texture(); + + VideoParams text_params = base ? base->params() : globals.vparams(); + text_params.set_format(PixelFormat::u8); + text_params.set_colorspace( + project()->color_manager()->get_default_input_color_space()); + + GenerateJob job(value); + job.insert(k_text_input, NodeValue(NodeValue::k_text, text)); + + push_mergable_job(value, Texture::job(text_params, job), table); + } else if (value.at(k_base_input).to_texture()) { + table->push(value.at(k_base_input)); + } +} + +void TextGeneratorV3::generate_frame(FramePtr frame, + const GenerateJob &job) const +{ + // Formerly laid out and rasterized with QTextDocument (fed through + // Html::html_to_doc) + QPainter directly over the frame buffer + // (QImage::Format_RGBA8888_Premultiplied, filled Qt::transparent). + // Layout/rasterization now runs behind the facade-installed text backend + // (see textbackend.h); the buffer handling and offset math are unchanged. + unsigned char *img = reinterpret_cast(frame->data()); + const int width = frame->width(); + const int height = frame->height(); + const int linesize = frame->linesize_bytes(); + + // Clear to transparent (formerly QImage::fill(Qt::transparent)) + for (int y = 0; y < height; y++) { + std::memset(img + size_t(linesize) * y, 0, size_t(width) * 4); + } + + if (TextMeasureBackend measure = text_measure_backend()) { + TextLayoutRequest req; + req.text = job.get(k_text_input).to_string(); + req.mode = TextLayoutRequest::k_olive_html; + // 96 DPI in DPM (96 / 2.54 * 100) + req.dots_per_meter = 3780; + + Vector2D size = job.get(k_size_input).to_vec2(); + req.wrap_width = size.x(); + + TextLayoutSize doc = measure(req); + + double scale = 1.0 / frame->video_params().divider(); + + Vector2D pos = + job.get(k_position_input).to_vec2(); + double base_offset_x = pos.x() - size.x() / 2 + + frame->video_params().width() / 2; + double base_offset_y = pos.y() - size.y() / 2 + + frame->video_params().height() / 2; + + double draw_offset_x = base_offset_x; + double draw_offset_y = base_offset_y; + + switch (static_cast( + job.get(k_vertical_alignment_input) + .to_int())) { + case k_v_align_top: + // Do nothing + break; + case k_v_align_middle: + draw_offset_y += size.y() / 2 - doc.height / 2; + break; + case k_v_align_bottom: + draw_offset_y += size.y() - doc.height; + break; + } + + if (TextRenderBackend render = text_render_backend()) { + TextRenderTransform t; + t.scale = scale; + t.draw_offset_x = draw_offset_x; + t.draw_offset_y = draw_offset_y; + // The clip rect was set before the vertical alignment translate + t.clip_enabled = true; + t.clip_offset_x = base_offset_x; + t.clip_offset_y = base_offset_y; + t.clip_width = size.x(); + t.clip_height = size.y(); + TextRenderTarget target = { img, width, height, linesize, 4 }; + render(req, t, target); + } + } else { + static bool warned = false; + if (!warned) { + fprintf(stderr, + "TextGeneratorV3: no text backend installed, frame left empty\n"); + warned = true; + } + } +} + +void TextGeneratorV3::update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) +{ + super::update_gizmo_positions(row, globals); + + // QPolygonF::boundingRect() over the gizmo polygon (QPolygonF is now + // std::vector) + RectF rect; + const std::vector &poly = poly_gizmo()->get_polygon(); + if (!poly.empty()) { + double min_x = poly.front().x(); + double min_y = poly.front().y(); + double max_x = min_x; + double max_y = min_y; + for (const PointF &p : poly) { + min_x = p.x() < min_x ? p.x() : min_x; + min_y = p.y() < min_y ? p.y() : min_y; + max_x = p.x() > max_x ? p.x() : max_x; + max_y = p.y() > max_y ? p.y() : max_y; + } + rect = RectF(min_x, min_y, max_x - min_x, max_y - min_y); + } + text_gizmo_->set_rect(rect); + text_gizmo_->set_html(row.at(k_text_input).to_string()); +} + +int TextGeneratorV3::get_qt_alignment_from_ours(VerticalAlignment v) +{ + switch (v) { + case k_v_align_top: + return TextGizmo::k_align_top; + case k_v_align_middle: + return TextGizmo::k_align_vcenter; + case k_v_align_bottom: + return TextGizmo::k_align_bottom; + } + return 0; +} + +TextGeneratorV3::VerticalAlignment +TextGeneratorV3::get_our_alignment_from_qts(int v) +{ + switch (v) { + case TextGizmo::k_align_top: + return k_v_align_top; + case TextGizmo::k_align_vcenter: + return k_v_align_middle; + case TextGizmo::k_align_bottom: + return k_v_align_bottom; + } + + return k_v_align_top; +} + +std::string TextGeneratorV3::format_string(const std::string &input, + const StringList &args) +{ + std::string output; + output.reserve(input.size()); + + // The original iterated UTF-16 code units; scanning bytes is equivalent + // here because '%' and the ASCII digits are single-byte in UTF-8 and + // multi-byte sequences are copied through verbatim + for (size_t i = 0; i < input.size(); i++) { + const char this_char = input.at(i); + + if (i < input.size() - 1 && this_char == '%') { + const char next_char = input.at(i + 1); + if (next_char == '%') { + // Double percent, append a single percent + output.push_back('%'); + i++; + } else if (next_char >= '0' && next_char <= '9') { + // Find length of number + std::string num; + i++; + while (i < input.size() && input.at(i) >= '0' && + input.at(i) <= '9') { + num.push_back(input.at(i)); + i++; + } + i--; + // QString::toInt() semantics: out-of-int-range parses fail + // and yield 0, making the index -1 + long long n = strtoll(num.c_str(), nullptr, 10); + int index = (n > INT32_MAX || n < INT32_MIN) ? -1 : int(n) - 1; + if (index >= 0 && index < int(args.size())) { + output.append(args.at(index)); + } + } else { + output.push_back(this_char); + } + } else { + output.push_back(this_char); + } + } + + return output; +} + +void TextGeneratorV3::InputValueChangedEvent(const std::string &input, int element) +{ + if (input == k_vertical_alignment_input && !dont_emit_valign_) { + text_gizmo_->set_vertical_alignment( + get_qt_alignment_from_ours(get_vertical_alignment())); + } + + super::InputValueChangedEvent(input, element); +} + +void TextGeneratorV3::gizmo_activated() +{ + set_standard_value(k_use_args_input, false); + // The TextGizmo vertical_alignment_changed signal connection was removed + // with QObject; the gizmo wave / facade invokes + // set_vertical_alignment_undoable() directly. + dont_emit_valign_ = true; +} + +void TextGeneratorV3::gizmo_deactivated() +{ + set_standard_value(k_use_args_input, true); + // See gizmo_activated() + dont_emit_valign_ = true; +} + +void TextGeneratorV3::set_vertical_alignment_undoable(int a) +{ + EngineCore::instance()->undo_stack()->push( + new NodeParamSetStandardValueCommand(NodeInput(this, + k_vertical_alignment_input), + int(get_our_alignment_from_qts(a))), + "Set Text Vertical Alignment"); +} + +} diff --git a/src/node/src/generator/text/textv3.h b/src/node/src/generator/text/textv3.h new file mode 100644 index 000000000..e8e3463b4 --- /dev/null +++ b/src/node/src/generator/text/textv3.h @@ -0,0 +1,95 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TEXTGENERATORV3_H +#define OAK_TEXTGENERATORV3_H + +#include "generator/shape/shapenodebase.h" +#include "gizmo/text.h" + +namespace olive +{ + +class TextGeneratorV3 : public ShapeNodeBase { +public: + TextGeneratorV3(); + + NODE_DEFAULT_FUNCTIONS(TextGeneratorV3) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void generate_frame(FramePtr frame, + const GenerateJob &job) const override; + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) override; + + enum VerticalAlignment { k_v_align_top, k_v_align_middle, k_v_align_bottom }; + + VerticalAlignment get_vertical_alignment() const + { + return static_cast( + get_standard_value(k_vertical_alignment_input).to_int()); + } + + // Alignment values travel as ints now (formerly Qt::Alignment), matching + // TextGizmo::VerticalAlignment (0 = top, 1 = bottom, 2 = vcenter) + static int get_qt_alignment_from_ours(VerticalAlignment v); + static VerticalAlignment get_our_alignment_from_qts(int v); + + static const std::string k_text_input; + static const std::string k_vertical_alignment_input; + static const std::string k_use_args_input; + static const std::string k_args_input; + + static std::string format_string(const std::string &input, + const StringList &args); + + /** @brief Access the text gizmo. */ + TextGizmo *text_gizmo() { return text_gizmo_; } + +protected: + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + +private: + TextGizmo *text_gizmo_; + + bool dont_emit_valign_; + + // Formerly private slots connected to TextGizmo signals; the gizmo wave / + // facade wires these up to the gizmo's notifications directly + void gizmo_activated(); + void gizmo_deactivated(); + void set_vertical_alignment_undoable(int a); +}; + +} + +#endif // OAK_TEXTGENERATORV3_H diff --git a/src/node/src/geometry.h b/src/node/src/geometry.h new file mode 100644 index 000000000..d93f27e56 --- /dev/null +++ b/src/node/src/geometry.h @@ -0,0 +1,132 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEGEOMETRY_H +#define OAK_NODEGEOMETRY_H + +#include + +#include "mathtypes.h" + +// NOTE: the de-Qt replacements for QRectF and QLineF live with their gizmo +// owners: olive::RectF in "node/gizmo/text.h", olive::LineF in +// "node/gizmo/line.h" (gizmo wave). QPolygonF is std::vector. + +namespace olive +{ + +/** + * @brief De-Qt replacement for QPainterPath (minimal recording POD). + * + * Records path elements only; rasterization is performed by the facade/app + * layer (see PathFillBackend below), which owns an actual rasterizer + * (formerly QPainter::drawPath with a white brush and no pen). + */ +class PainterPath { +public: + struct Element { + enum Type { + k_move_to, + k_line_to, + k_cubic_to // p1/p2 = control points, p3 = end point + }; + + Type type; + PointF p1; + PointF p2; + PointF p3; + }; + + void move_to(const PointF &p) + { + elements_.push_back({ Element::k_move_to, p, PointF(), PointF() }); + } + + void line_to(const PointF &p) + { + elements_.push_back({ Element::k_line_to, p, PointF(), PointF() }); + } + + void cubic_to(const PointF &c1, const PointF &c2, const PointF &end) + { + elements_.push_back({ Element::k_cubic_to, c1, c2, end }); + } + + bool empty() const + { + return elements_.empty(); + } + + const std::vector &elements() const + { + return elements_; + } + + /** + * @brief QPainterPath::translated() equivalent (returns a copy). + */ + PainterPath translated(const PointF &offset) const + { + PainterPath copy = *this; + for (Element &e : copy.elements_) { + e.p1 += offset; + e.p2 += offset; + e.p3 += offset; + } + return copy; + } + +private: + std::vector elements_; +}; + +/** + * @brief Backend hook for filling a PainterPath into an 8-bit RGBA + * premultiplied buffer (replaces the QPainter rasterization in + * generator nodes' generate_frame()). + * + * The transform mirrors the original QPainter calls `scale(sx, sy)` followed + * by `translate(tx, ty)`, i.e. a path point p lands at + * ((p.x + tx) * sx, (p.y + ty) * sy). The path is filled solid white + * (formerly QPainter with Qt::white brush and Qt::NoPen). + * + * Installed by the facade/app layer via set_path_fill_backend(). When no + * backend is installed, callers leave the (already cleared) buffer untouched. + */ +using PathFillBackend = void (*)(const PainterPath &path, double scale_x, + double scale_y, double translate_x, + double translate_y, unsigned char *rgba, + int width, int height, int linesize_bytes); + +inline PathFillBackend g_path_fill_backend = nullptr; + +inline void set_path_fill_backend(PathFillBackend backend) +{ + g_path_fill_backend = backend; +} + +inline PathFillBackend path_fill_backend() +{ + return g_path_fill_backend; +} + +} + +#endif // OAK_NODEGEOMETRY_H diff --git a/src/node/src/gizmo/CMakeLists.txt b/src/node/src/gizmo/CMakeLists.txt new file mode 100644 index 000000000..8c9b6aa75 --- /dev/null +++ b/src/node/src/gizmo/CMakeLists.txt @@ -0,0 +1,36 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/gizmo/draggable.cpp + node/gizmo/draggable.h + node/gizmo/gizmo.cpp + node/gizmo/gizmo.h + node/gizmo/line.cpp + node/gizmo/line.h + node/gizmo/path.cpp + node/gizmo/path.h + node/gizmo/point.cpp + node/gizmo/point.h + node/gizmo/polygon.cpp + node/gizmo/polygon.h + node/gizmo/screen.cpp + node/gizmo/screen.h + node/gizmo/text.cpp + node/gizmo/text.h + PARENT_SCOPE +) diff --git a/src/node/src/gizmo/draggable.cpp b/src/node/src/gizmo/draggable.cpp new file mode 100644 index 000000000..78b8e0a2d --- /dev/null +++ b/src/node/src/gizmo/draggable.cpp @@ -0,0 +1,61 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "draggable.h" + +#include "node.h" + +namespace olive +{ + +DraggableGizmo::DraggableGizmo(Node *parent) + : NodeGizmo{ parent } + , drag_value_behavior_(k_absolute) +{ +} + +void DraggableGizmo::drag_start(const NodeValueRow &row, double abs_x, + double abs_y, const Rational &time) +{ + for (int i = 0; i < int(draggers_.size()); i++) { + draggers_[i].start(inputs_[i], time); + } + + if (Node *p = parent_node()) { + p->gizmo_drag_start(row, abs_x, abs_y, time); + } +} + +void DraggableGizmo::drag_move(double x, double y, int modifiers) +{ + if (Node *p = parent_node()) { + p->gizmo_drag_move(x, y, modifiers); + } +} + +void DraggableGizmo::drag_end(MultiUndoCommand *command) +{ + for (int i = 0; i < int(draggers_.size()); i++) { + draggers_[i].end(command); + } +} + +} diff --git a/src/node/src/gizmo/draggable.h b/src/node/src/gizmo/draggable.h new file mode 100644 index 000000000..2f5fda8e9 --- /dev/null +++ b/src/node/src/gizmo/draggable.h @@ -0,0 +1,85 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_DRAGGABLEGIZMO_H +#define OAK_DRAGGABLEGIZMO_H + +#include "gizmo.h" +#include "inputdragger.h" +#include "undocommand.h" + +namespace olive +{ + +class DraggableGizmo : public NodeGizmo { +public: + /// Changes what the X/Y coordinates emitted from HandleMovement specify + enum DragValueBehavior { + /// X/Y will be the exact mouse coordinates (in sequence pixels) + k_absolute, + + /// X/Y will be the movement since the last time HandleMovement was called + k_delta_from_previous, + + /// X/Y will be the movement from the start of the drag + k_delta_from_start + }; + + explicit DraggableGizmo(Node *parent = nullptr); + + void drag_start(const NodeValueRow &row, double abs_x, double abs_y, + const olive::core::Rational &time); + + void drag_move(double x, double y, int modifiers); + + void drag_end(olive::MultiUndoCommand *command); + + void add_input(const NodeKeyframeTrackReference &input) + { + inputs_.push_back(input); + draggers_.push_back(NodeInputDragger()); + } + + std::vector &get_draggers() + { + return draggers_; + } + + DragValueBehavior get_drag_value_behavior() const + { + return drag_value_behavior_; + } + void set_drag_value_behavior(DragValueBehavior d) + { + drag_value_behavior_ = d; + } + +private: + std::vector inputs_; + + std::vector draggers_; + + DragValueBehavior drag_value_behavior_; +}; + +} + +#endif // OAK_DRAGGABLEGIZMO_H diff --git a/src/node/src/gizmo/gizmo.cpp b/src/node/src/gizmo/gizmo.cpp new file mode 100644 index 000000000..5c1f0fa67 --- /dev/null +++ b/src/node/src/gizmo/gizmo.cpp @@ -0,0 +1,42 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "gizmo.h" + +#include "node.h" + +namespace olive +{ + +NodeGizmo::NodeGizmo(Node *parent) + : parent_(parent) + , visible_(true) +{ +} + +NodeGizmo::~NodeGizmo() +{ + if (parent_) { + parent_->remove_gizmo(this); + } +} + +} diff --git a/src/node/src/gizmo/gizmo.h b/src/node/src/gizmo/gizmo.h new file mode 100644 index 000000000..e774c2555 --- /dev/null +++ b/src/node/src/gizmo/gizmo.h @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEGIZMO_H +#define OAK_NODEGIZMO_H + +#include "globals.h" + +namespace olive +{ + +class Node; + +class NodeGizmo { +public: + explicit NodeGizmo(Node *parent = nullptr); + virtual ~NodeGizmo(); + + /** + * @brief Owning node (replaces QObject parent) + * + * The constructor does NOT register with the node; registration happens + * through Node::add_gizmo() (Node::add_draggable_gizmo() already calls + * it). The destructor unregisters via Node::remove_gizmo(), mirroring + * the original setParent(nullptr) in the destructor. + */ + Node *parent_node() const + { + return parent_; + } + + const NodeGlobals &get_globals() const + { + return globals_; + } + void set_globals(const NodeGlobals &globals) + { + globals_ = globals; + } + + bool is_visible() const + { + return visible_; + } + void set_visible(bool e) + { + visible_ = e; + } + +private: + Node *parent_; + + NodeGlobals globals_; + + bool visible_; +}; + +} + +#endif // OAK_NODEGIZMO_H diff --git a/src/node/src/gizmo/line.cpp b/src/node/src/gizmo/line.cpp new file mode 100644 index 000000000..f6f91e441 --- /dev/null +++ b/src/node/src/gizmo/line.cpp @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "line.h" + +namespace olive +{ + +LineGizmo::LineGizmo(Node *parent) + : NodeGizmo(parent) +{ +} + +} diff --git a/src/node/src/gizmo/line.h b/src/node/src/gizmo/line.h new file mode 100644 index 000000000..014236ef0 --- /dev/null +++ b/src/node/src/gizmo/line.h @@ -0,0 +1,79 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LINEGIZMO_H +#define OAK_LINEGIZMO_H + +#include "gizmo.h" +#include "mathtypes.h" + +namespace olive +{ + +/** + * @brief De-Qt replacement for QLineF (data carrier only). + */ +class LineF { +public: + LineF() + { + } + + LineF(const PointF &p1, const PointF &p2) + : p1_(p1) + , p2_(p2) + { + } + + const PointF &p1() const + { + return p1_; + } + const PointF &p2() const + { + return p2_; + } + +private: + PointF p1_; + PointF p2_; +}; + +class LineGizmo : public NodeGizmo { +public: + LineGizmo(Node *parent = nullptr); + + const LineF &get_line() const + { + return line_; + } + void set_line(const LineF &line) + { + line_ = line; + } + +private: + LineF line_; +}; + +} + +#endif // OAK_LINEGIZMO_H diff --git a/src/node/src/gizmo/path.cpp b/src/node/src/gizmo/path.cpp new file mode 100644 index 000000000..0da438132 --- /dev/null +++ b/src/node/src/gizmo/path.cpp @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "path.h" + +namespace olive +{ + +PathGizmo::PathGizmo(Node *parent) + : DraggableGizmo{ parent } +{ +} + +} diff --git a/src/node/src/gizmo/path.h b/src/node/src/gizmo/path.h new file mode 100644 index 000000000..e2f79051c --- /dev/null +++ b/src/node/src/gizmo/path.h @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PATHGIZMO_H +#define OAK_PATHGIZMO_H + +#include "draggable.h" + +namespace olive +{ + +/** + * @brief Draggable gizmo formerly backed by a QPainterPath + * + * The QPainterPath member (a drawing/hit-test primitive) has been removed; + * path storage and drawing belong to the app layer. The class shell is kept + * so the gizmo type hierarchy remains distinguishable. + */ +class PathGizmo : public DraggableGizmo { +public: + explicit PathGizmo(Node *parent = nullptr); +}; + +} + +#endif // OAK_PATHGIZMO_H diff --git a/src/node/src/gizmo/point.cpp b/src/node/src/gizmo/point.cpp new file mode 100644 index 000000000..216b1fa51 --- /dev/null +++ b/src/node/src/gizmo/point.cpp @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "point.h" + +namespace olive +{ + +PointGizmo::PointGizmo(const Shape &shape, bool smaller, Node *parent) + : DraggableGizmo{ parent } + , shape_(shape) + , smaller_(smaller) +{ +} + +PointGizmo::PointGizmo(const Shape &shape, Node *parent) + : PointGizmo(shape, false, parent) +{ +} + +PointGizmo::PointGizmo(Node *parent) + : PointGizmo(k_square, parent) +{ +} + +} diff --git a/src/node/src/gizmo/point.h b/src/node/src/gizmo/point.h new file mode 100644 index 000000000..b4194a718 --- /dev/null +++ b/src/node/src/gizmo/point.h @@ -0,0 +1,77 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_POINTGIZMO_H +#define OAK_POINTGIZMO_H + +#include "draggable.h" +#include "mathtypes.h" + +namespace olive +{ + +class PointGizmo : public DraggableGizmo { +public: + enum Shape { k_square, k_circle, k_anchor_point }; + + explicit PointGizmo(const Shape &shape, bool smaller, + Node *parent = nullptr); + explicit PointGizmo(const Shape &shape, Node *parent = nullptr); + explicit PointGizmo(Node *parent = nullptr); + + const Shape &get_shape() const + { + return shape_; + } + void set_shape(const Shape &s) + { + shape_ = s; + } + + const PointF &get_point() const + { + return point_; + } + void set_point(const PointF &pt) + { + point_ = pt; + } + + bool get_smaller() const + { + return smaller_; + } + void set_smaller(bool e) + { + smaller_ = e; + } + +private: + Shape shape_; + + PointF point_; + + bool smaller_; +}; + +} + +#endif // OAK_POINTGIZMO_H diff --git a/src/node/src/gizmo/polygon.cpp b/src/node/src/gizmo/polygon.cpp new file mode 100644 index 000000000..4efda8908 --- /dev/null +++ b/src/node/src/gizmo/polygon.cpp @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "polygon.h" + +namespace olive +{ + +PolygonGizmo::PolygonGizmo(Node *parent) + : DraggableGizmo{ parent } +{ +} + +} diff --git a/src/node/src/gizmo/polygon.h b/src/node/src/gizmo/polygon.h new file mode 100644 index 000000000..a8531028a --- /dev/null +++ b/src/node/src/gizmo/polygon.h @@ -0,0 +1,54 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_POLYGONGIZMO_H +#define OAK_POLYGONGIZMO_H + +#include + +#include "draggable.h" +#include "mathtypes.h" + +namespace olive +{ + +class PolygonGizmo : public DraggableGizmo { +public: + explicit PolygonGizmo(Node *parent = nullptr); + + // De-Qt: QPolygonF is stored as a plain vector of points. Point-in-polygon + // testing (formerly QPolygonF::containsPoint) belongs to the app layer. + const std::vector &get_polygon() const + { + return polygon_; + } + void set_polygon(const std::vector &polygon) + { + polygon_ = polygon; + } + +private: + std::vector polygon_; +}; + +} + +#endif // OAK_POLYGONGIZMO_H diff --git a/src/node/src/gizmo/screen.cpp b/src/node/src/gizmo/screen.cpp new file mode 100644 index 000000000..4537de486 --- /dev/null +++ b/src/node/src/gizmo/screen.cpp @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "screen.h" + +namespace olive +{ + +ScreenGizmo::ScreenGizmo(Node *parent) + : DraggableGizmo{ parent } +{ +} + +} diff --git a/src/node/src/gizmo/screen.h b/src/node/src/gizmo/screen.h new file mode 100644 index 000000000..859ad2a60 --- /dev/null +++ b/src/node/src/gizmo/screen.h @@ -0,0 +1,37 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SCREENGIZMO_H +#define OAK_SCREENGIZMO_H + +#include "draggable.h" + +namespace olive +{ + +class ScreenGizmo : public DraggableGizmo { +public: + explicit ScreenGizmo(Node *parent = nullptr); +}; + +} + +#endif // OAK_SCREENGIZMO_H diff --git a/src/node/src/gizmo/text.cpp b/src/node/src/gizmo/text.cpp new file mode 100644 index 000000000..248e76a83 --- /dev/null +++ b/src/node/src/gizmo/text.cpp @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "text.h" + +#include "coreengine.h" +#include "node.h" +#include "undocommand.h" + +namespace olive +{ + +TextGizmo::TextGizmo(Node *parent) + : NodeGizmo{ parent } + , valign_(k_align_top) +{ +} + +void TextGizmo::set_rect(const RectF &r) +{ + rect_ = r; +} + +void TextGizmo::update_input_html(const std::string &s, const Rational &time) +{ + if (input_.is_valid()) { + MultiUndoCommand *command = new MultiUndoCommand(); + Node::set_value_at_time(input_.input(), time, s, input_.track(), command, + true); + EngineCore::instance()->undo_stack()->push(command, "Edit Text"); + } +} + +void TextGizmo::set_vertical_alignment(int va) +{ + valign_ = va; +} + +} diff --git a/src/node/src/gizmo/text.h b/src/node/src/gizmo/text.h new file mode 100644 index 000000000..8d2c4e89a --- /dev/null +++ b/src/node/src/gizmo/text.h @@ -0,0 +1,126 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TEXTGIZMO_H +#define OAK_TEXTGIZMO_H + +#include + +#include "gizmo.h" +#include "param.h" + +namespace olive +{ + +/** + * @brief De-Qt replacement for QRectF (data carrier only, text gizmo rect). + */ +class RectF { +public: + RectF() + : x_(0.0) + , y_(0.0) + , width_(0.0) + , height_(0.0) + { + } + + RectF(double x, double y, double w, double h) + : x_(x) + , y_(y) + , width_(w) + , height_(h) + { + } + + double x() const + { + return x_; + } + double y() const + { + return y_; + } + double width() const + { + return width_; + } + double height() const + { + return height_; + } + +private: + double x_; + double y_; + double width_; + double height_; +}; + +class TextGizmo : public NodeGizmo { +public: + /// Vertical alignment values (formerly Qt::Alignment), matching the + /// oakengine facade: 0 = AlignTop, 1 = AlignBottom, 2 = AlignVCenter + enum VerticalAlignment { k_align_top = 0, k_align_bottom = 1, k_align_vcenter = 2 }; + + explicit TextGizmo(Node *parent = nullptr); + + const RectF &get_rect() const + { + return rect_; + } + void set_rect(const RectF &r); + + const std::string &get_html() const + { + return text_; + } + void set_html(const std::string &t) + { + text_ = t; + } + + void set_input(const NodeKeyframeTrackReference &input) + { + input_ = input; + } + + void update_input_html(const std::string &s, const Rational &time); + + int get_vertical_alignment() const + { + return valign_; + } + void set_vertical_alignment(int va); + +private: + RectF rect_; + + std::string text_; + + NodeKeyframeTrackReference input_; + + int valign_; +}; + +} + +#endif // OAK_TEXTGIZMO_H diff --git a/src/node/src/globals.cpp b/src/node/src/globals.cpp new file mode 100644 index 000000000..ed1da481b --- /dev/null +++ b/src/node/src/globals.cpp @@ -0,0 +1,27 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "globals.h" + +namespace olive +{ + +} diff --git a/src/node/src/globals.h b/src/node/src/globals.h new file mode 100644 index 000000000..abc2b1601 --- /dev/null +++ b/src/node/src/globals.h @@ -0,0 +1,95 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEGLOBALS_H +#define OAK_NODEGLOBALS_H + +#include "mathtypes.h" + +#include "olive/core/util/timerange.h" +#include "olive/core/render/audioparams.h" +#include "loopmode.h" +#include "videoparams.h" + +namespace olive +{ + +using core::AudioParams; +using core::TimeRange; + +class NodeGlobals { +public: + NodeGlobals() + { + } + + NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, + const TimeRange &time, LoopMode loop_mode) + : video_params_(vparam) + , audio_params_(aparam) + , time_(time) + , loop_mode_(loop_mode) + { + } + + NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, + const Rational &time, LoopMode loop_mode) + : NodeGlobals(vparam, aparam, + TimeRange(time, time + vparam.frame_rate_as_time_base()), + loop_mode) + { + } + + Vector2D square_resolution() const + { + return Vector2D(float(video_params_.square_pixel_width()), float(video_params_.height())); + } + Vector2D nonsquare_resolution() const + { + return Vector2D(float(video_params_.width()), float(video_params_.height())); + } + const AudioParams &aparams() const + { + return audio_params_; + } + const VideoParams &vparams() const + { + return video_params_; + } + const TimeRange &time() const + { + return time_; + } + LoopMode loop_mode() const + { + return loop_mode_; + } + +private: + VideoParams video_params_; + AudioParams audio_params_; + TimeRange time_; + LoopMode loop_mode_; +}; + +} + +#endif // OAK_NODEGLOBALS_H diff --git a/src/node/src/group/CMakeLists.txt b/src/node/src/group/CMakeLists.txt new file mode 100644 index 000000000..9eebaf064 --- /dev/null +++ b/src/node/src/group/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/group/group.cpp + node/group/group.h + PARENT_SCOPE +) diff --git a/src/node/src/group/group.cpp b/src/node/src/group/group.cpp new file mode 100644 index 000000000..0bb29a70e --- /dev/null +++ b/src/node/src/group/group.cpp @@ -0,0 +1,391 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "group.h" + +#include +#include +#include + +#include "serializeddata.h" + +namespace olive +{ + +#define super Node + +NodeGroup::NodeGroup() + : output_passthrough_(nullptr) +{ + set_flag(k_dont_show_in_create_menu); +} + +std::string NodeGroup::name() const +{ + return "Group"; +} + +std::string NodeGroup::id() const +{ + return "org.olivevideoeditor.Olive.group"; +} + +std::vector NodeGroup::category() const +{ + return { k_category_unknown }; +} + +std::string NodeGroup::description() const +{ + return "A group of nodes that is represented as a single node."; +} + +void NodeGroup::retranslate() +{ + super::retranslate(); + + for (auto it = get_context_positions().cbegin(); + it != get_context_positions().cend(); it++) { + it->first->retranslate(); + } +} + +bool NodeGroup::load_custom(XmlStreamReader *reader, SerializedData *data) +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthroughs") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthrough") { + SerializedData::GroupLink link; + + link.group = this; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + link.input_node = strtoull( + reader->read_element_text().c_str(), nullptr, 10); + } else if (reader->name() == "input") { + link.input_id = reader->read_element_text(); + } else if (reader->name() == "element") { + link.input_element = int(strtol( + reader->read_element_text().c_str(), nullptr, 10)); + } else if (reader->name() == "id") { + link.passthrough_id = reader->read_element_text(); + } else if (reader->name() == "name") { + link.custom_name = reader->read_element_text(); + } else if (reader->name() == "flags") { + link.custom_flags = InputFlags(strtoull( + reader->read_element_text().c_str(), nullptr, 10)); + } else if (reader->name() == "type") { + link.data_type = NodeValue::get_data_type_from_name( + reader->read_element_text()); + } else if (reader->name() == "default") { + link.default_val = NodeValue::string_to_value( + link.data_type, reader->read_element_text(), + false); + } else if (reader->name() == "properties") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "property") { + std::string key; + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "key") { + key = reader->read_element_text(); + } else if (reader->name() == "value") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + if (!key.empty()) { + link.custom_properties.insert({ key, + value }); + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + data->group_input_links.push_back(link); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "outputpassthrough") { + data->group_output_links.insert( + { this, strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void NodeGroup::save_custom(XmlStreamWriter *writer) const +{ + writer->write_start_element("inputpassthroughs"); + + for (const NodeGroup::InputPassthrough &ip : + this->get_input_passthroughs()) { + writer->write_start_element("inputpassthrough"); + + // Reference to inner input + writer->write_text_element( + "node", + std::to_string(reinterpret_cast(ip.second.node()))); + writer->write_text_element("input", ip.second.input()); + writer->write_text_element("element", + std::to_string(ip.second.element())); + + // ID of passthrough + writer->write_text_element("id", ip.first); + + // Passthrough-specific details + const std::string &input = ip.first; + writer->write_text_element("name", this->Node::get_input_name(input)); + + writer->write_text_element( + "flags", + std::to_string( + (get_input_flags(input) & ~ip.second.get_flags()).value())); + + NodeValue::Type data_type = get_input_data_type(input); + writer->write_text_element("type", + NodeValue::get_data_type_name(data_type)); + + writer->write_text_element( + "default", + NodeValue::value_to_string(data_type, get_default_value(input), false)); + + writer->write_start_element("properties"); + auto p = get_input_properties(input); + for (auto it = p.cbegin(); it != p.cend(); it++) { + writer->write_start_element("property"); + writer->write_text_element("key", it->first); + writer->write_text_element("value", it->second.to_string()); + writer->write_end_element(); // property + } + writer->write_end_element(); // properties + + writer->write_end_element(); // input + } + + writer->write_end_element(); // inputpassthroughs + + writer->write_text_element( + "outputpassthrough", + std::to_string( + reinterpret_cast(this->get_output_passthrough()))); +} + +void NodeGroup::PostLoadEvent(SerializedData *data) +{ + super::PostLoadEvent(data); + + for (const SerializedData::GroupLink &l : data->group_input_links) { + auto node_it = data->node_ptrs.find(l.input_node); + if (node_it != data->node_ptrs.end()) { + Node *input_node = node_it->second; + NodeInput resolved(input_node, l.input_id, l.input_element); + + l.group->add_input_passthrough(resolved, l.passthrough_id); + + l.group->set_input_flag(l.passthrough_id, + InputFlag(l.custom_flags.value())); + + if (!l.custom_name.empty()) { + l.group->set_input_name(l.passthrough_id, l.custom_name); + } + + l.group->set_input_data_type(l.passthrough_id, l.data_type); + + l.group->set_default_value(l.passthrough_id, l.default_val); + + for (auto it = l.custom_properties.cbegin(); + it != l.custom_properties.cend(); it++) { + l.group->set_input_property(l.passthrough_id, it->first, + it->second); + } + } + } + + for (auto it = data->group_output_links.cbegin(); + it != data->group_output_links.cend(); it++) { + auto node_it = data->node_ptrs.find(it->second); + if (node_it != data->node_ptrs.end()) { + it->first->set_output_passthrough(node_it->second); + } + } +} + +std::string NodeGroup::add_input_passthrough(const NodeInput &input, + const std::string &force_id) +{ + assert(context_contains_node(input.node())); + + for (auto it = input_passthroughs_.cbegin(); + it != input_passthroughs_.cend(); it++) { + if (it->second == input) { + // Already passing this input through + return it->first; + } + } + + // Add input + std::string id; + if (force_id.empty()) { + id = input.input(); + int i = 2; + while (has_input_with_id(id)) { + id = input.input() + "_" + std::to_string(i); + i++; + } + } else { + id = force_id; + + bool already_exists = false; + for (auto it = input_passthroughs_.cbegin(); + it != input_passthroughs_.cend(); it++) { + if (it->first == id) { + already_exists = true; + break; + } + } + + assert(!already_exists); + } + + add_input(id, input.get_data_type(), input.get_default_value(), + input.get_flags()); + + input_passthroughs_.push_back({ id, input }); + + return id; +} + +void NodeGroup::remove_input_passthrough(const NodeInput &input) +{ + for (auto it = input_passthroughs_.begin(); it != input_passthroughs_.end(); + it++) { + if (it->second == input) { + remove_input(it->first); + input_passthroughs_.erase(it); + break; + } + } +} + +void NodeGroup::set_output_passthrough(Node *node) +{ + assert(!node || context_contains_node(node)); + + output_passthrough_ = node; +} + +bool NodeGroup::contains_input_passthrough(const NodeInput &input) const +{ + for (auto it = input_passthroughs_.cbegin(); + it != input_passthroughs_.cend(); it++) { + if (it->second == input) { + return true; + } + } + + return false; +} + +std::string NodeGroup::get_input_name(const std::string &id) const +{ + // If an override name was set, use that + std::string override = super::get_input_name(id); + if (!override.empty()) { + return override; + } + + // Call GetInputName of passed through node, which may be another group + NodeInput pass = get_input_from_id(id); + if (!pass.is_valid()) { + return std::string(); + } + return pass.node()->get_input_name(pass.input()); +} + +NodeInput NodeGroup::resolve_input(NodeInput input) +{ + while (get_inner(&input)) { + } + + return input; +} + +bool NodeGroup::get_inner(NodeInput *input) +{ + if (NodeGroup *g = dynamic_cast(input->node())) { + const NodeInput &passthrough = g->get_input_from_id(input->input()); + if (!passthrough.is_valid()) { + return false; + } + + input->set_node(passthrough.node()); + input->set_input(passthrough.input()); + return true; + } else { + return false; + } +} + +void NodeGroupAddInputPassthrough::redo() +{ + if (!group_->contains_input_passthrough(input_)) { + group_->add_input_passthrough(input_, force_id_); + actually_added_ = true; + } else { + actually_added_ = false; + } +} + +void NodeGroupAddInputPassthrough::undo() +{ + if (actually_added_) { + group_->remove_input_passthrough(input_); + } +} + +void NodeGroupSetOutputPassthrough::redo() +{ + old_output_ = group_->get_output_passthrough(); + group_->set_output_passthrough(new_output_); +} + +void NodeGroupSetOutputPassthrough::undo() +{ + group_->set_output_passthrough(old_output_); +} + +} diff --git a/src/node/src/group/group.h b/src/node/src/group/group.h new file mode 100644 index 000000000..5f214e1a8 --- /dev/null +++ b/src/node/src/group/group.h @@ -0,0 +1,156 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEGROUP_H +#define OAK_NODEGROUP_H + +#include +#include +#include + +#include "node.h" + +namespace olive +{ + +class NodeGroup : public Node { +public: + NodeGroup(); + + NODE_DEFAULT_FUNCTIONS(NodeGroup) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual bool load_custom(XmlStreamReader *reader, + SerializedData *data) override; + virtual void save_custom(XmlStreamWriter *writer) const override; + virtual void PostLoadEvent(SerializedData *data) override; + + std::string add_input_passthrough(const NodeInput &input, + const std::string &force_id = std::string()); + + void remove_input_passthrough(const NodeInput &input); + + Node *get_output_passthrough() const + { + return output_passthrough_; + } + + void set_output_passthrough(Node *node); + + using InputPassthrough = std::pair; + using InputPassthroughs = std::vector; + const InputPassthroughs &get_input_passthroughs() const + { + return input_passthroughs_; + } + + bool contains_input_passthrough(const NodeInput &input) const; + + virtual std::string get_input_name(const std::string &id) const override; + + static NodeInput resolve_input(NodeInput input); + static bool get_inner(NodeInput *input); + + std::string get_id_of_passthrough(const NodeInput &input) const + { + for (auto it = input_passthroughs_.cbegin(); + it != input_passthroughs_.cend(); it++) { + if (it->second == input) { + return it->first; + } + } + return std::string(); + } + + NodeInput get_input_from_id(const std::string &id) const + { + for (auto it = input_passthroughs_.cbegin(); + it != input_passthroughs_.cend(); it++) { + if (it->first == id) { + return it->second; + } + } + return NodeInput(); + } + +private: + InputPassthroughs input_passthroughs_; + + Node *output_passthrough_; +}; + +class NodeGroupAddInputPassthrough : public UndoCommand { +public: + NodeGroupAddInputPassthrough(NodeGroup *group, const NodeInput &input, + const std::string &force_id = std::string()) + : group_(group) + , input_(input) + , actually_added_(false) + { + // NOTE: upstream Olive never stored force_id here (force_id_ stays + // empty); preserved as-is for behavior neutrality. + (void) force_id; + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeGroup *group_; + + NodeInput input_; + + std::string force_id_; + + bool actually_added_; +}; + +class NodeGroupSetOutputPassthrough : public UndoCommand { +public: + NodeGroupSetOutputPassthrough(NodeGroup *group, Node *output) + : group_(group) + , new_output_(output) + { + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeGroup *group_; + + Node *new_output_; + Node *old_output_; +}; + +} + +#endif // OAK_NODEGROUP_H diff --git a/src/node/src/input/CMakeLists.txt b/src/node/src/input/CMakeLists.txt new file mode 100644 index 000000000..28d4d34c8 --- /dev/null +++ b/src/node/src/input/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(multicam) +add_subdirectory(time) +add_subdirectory(value) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/input/multicam/CMakeLists.txt b/src/node/src/input/multicam/CMakeLists.txt new file mode 100644 index 000000000..a8fe63ad4 --- /dev/null +++ b/src/node/src/input/multicam/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/input/multicam/multicamnode.h + node/input/multicam/multicamnode.cpp + PARENT_SCOPE +) diff --git a/src/node/src/input/multicam/multicamnode.cpp b/src/node/src/input/multicam/multicamnode.cpp new file mode 100644 index 000000000..c5eb2ae1b --- /dev/null +++ b/src/node/src/input/multicam/multicamnode.cpp @@ -0,0 +1,206 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "multicamnode.h" + +#include "project/sequence/sequence.h" + +namespace olive +{ + +#define super Node + +const std::string MultiCamNode::k_current_input = "current_in"; +const std::string MultiCamNode::k_sources_input = "sources_in"; +const std::string MultiCamNode::k_sequence_input = "sequence_in"; +const std::string MultiCamNode::k_sequence_type_input = "sequence_type_in"; + +MultiCamNode::MultiCamNode() +{ + add_input(k_current_input, NodeValue::k_combo, InputFlags(k_input_flag_static)); + + add_input(k_sources_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable | k_input_flag_array)); + set_input_property(k_sources_input, "arraystart", 1); + + add_input(k_sequence_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); + add_input(k_sequence_type_input, NodeValue::k_combo, + InputFlags(k_input_flag_static | k_input_flag_hidden)); + + sequence_ = nullptr; +} + +std::string MultiCamNode::name() const +{ + return "Multi-Cam"; +} + +std::string MultiCamNode::id() const +{ + return "org.olivevideoeditor.Olive.multicam"; +} + +std::vector MultiCamNode::category() const +{ + return { k_category_timeline }; +} + +std::string MultiCamNode::description() const +{ + return "Allows easy switching between multiple sources."; +} + +Node::ActiveElements +MultiCamNode::get_active_elements_at_time(const std::string &input, + const TimeRange &r) const +{ + if (input == k_sources_input) { + int src = get_current_source(); + if (src >= 0 && src < get_source_count()) { + Node::ActiveElements a; + a.add(src); + return a; + } else { + return ActiveElements::k_no_elements; + } + } else { + return super::get_active_elements_at_time(input, r); + } +} + +void MultiCamNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + NodeValueArray arr = value.at(k_sources_input).to_array(); + if (!arr.empty()) { + table->push(arr.begin()->second); + } +} + +void MultiCamNode::index_to_row_cols(int index, int total_rows, int total_cols, + int *row, int *col) +{ + (void) total_rows; + + *col = index % total_cols; + *row = index / total_cols; +} + +Node *MultiCamNode::get_connected_render_output(const std::string &input, + int element) const +{ + if (sequence_ && input == k_sources_input && element >= 0 && + element < get_source_count()) { + return get_track_list()->get_track_at(element); + } else { + return Node::get_connected_render_output(input, element); + } +} + +bool MultiCamNode::is_input_connected_for_render(const std::string &input, + int element) const +{ + if (sequence_ && input == k_sources_input && element >= 0 && + element < get_source_count()) { + return true; + } else { + return Node::is_input_connected_for_render(input, element); + } +} + +std::vector MultiCamNode::ignore_inputs_for_rendering() const +{ + return { k_sequence_input }; +} + +void MultiCamNode::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + if (input == k_sequence_input) { + if (Sequence *s = dynamic_cast(output)) { + set_input_flag(k_sequence_type_input, k_input_flag_hidden, false); + sequence_ = s; + } + } +} + +void MultiCamNode::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + if (input == k_sequence_input) { + set_input_flag(k_sequence_type_input, k_input_flag_hidden, true); + sequence_ = nullptr; + } +} + +TrackList *MultiCamNode::get_track_list() const +{ + return sequence_->track_list( + static_cast(get_standard_value(k_sequence_type_input).to_int())); +} + +void MultiCamNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_current_input, "Current"); + set_input_name(k_sources_input, "Sources"); + set_input_name(k_sequence_input, "Sequence"); + set_input_name(k_sequence_type_input, "Sequence Type"); + set_combo_box_strings(k_sequence_type_input, { "Video", "Audio" }); + + StringList names; + int name_count = get_source_count(); + names.reserve(name_count); + for (int i = 0; i < name_count; i++) { + std::string src_name; + if (Node *n = get_connected_render_output(k_sources_input, i)) { + src_name = n->name(); + } + names.push_back(std::to_string(i + 1) + ": " + src_name); + } + set_combo_box_strings(k_current_input, names); +} + +int MultiCamNode::get_source_count() const +{ + if (sequence_) { + return get_track_list()->get_track_count(); + } else { + return input_array_size(k_sources_input); + } +} + +void MultiCamNode::get_rows_and_columns(int sources, int *rows_in, int *cols_in) +{ + int &rows = *rows_in; + int &cols = *cols_in; + + rows = 1; + cols = 1; + while (rows * cols < sources) { + if (rows < cols) { + rows++; + } else { + cols++; + } + } +} + +} diff --git a/src/node/src/input/multicam/multicamnode.h b/src/node/src/input/multicam/multicamnode.h new file mode 100644 index 000000000..96ffb1c06 --- /dev/null +++ b/src/node/src/input/multicam/multicamnode.h @@ -0,0 +1,102 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_MULTICAMNODE_H +#define OAK_MULTICAMNODE_H + +#include "node.h" +#include "output/track/tracklist.h" + +namespace olive +{ + +class Sequence; + +class MultiCamNode : public Node { +public: + MultiCamNode(); + + NODE_DEFAULT_FUNCTIONS(MultiCamNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual ActiveElements + get_active_elements_at_time(const std::string &input, + const TimeRange &r) const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void retranslate() override; + + static const std::string k_current_input; + static const std::string k_sources_input; + static const std::string k_sequence_input; + static const std::string k_sequence_type_input; + + int get_current_source() const + { + return get_standard_value(k_current_input).to_int(); + } + + int get_source_count() const; + + static void get_rows_and_columns(int sources, int *rows, int *cols); + void get_rows_and_columns(int *rows, int *cols) const + { + return get_rows_and_columns(get_source_count(), rows, cols); + } + + void set_sequence_type(Track::Type t) + { + set_standard_value(k_sequence_type_input, int(t)); + } + + static void index_to_row_cols(int index, int total_rows, int total_cols, + int *row, int *col); + + static int rows_cols_to_index(int row, int col, int total_rows, int total_cols) + { + return col + row * total_cols; + } + + virtual Node *get_connected_render_output(const std::string &input, + int element = -1) const override; + virtual bool is_input_connected_for_render(const std::string &input, + int element = -1) const override; + + virtual std::vector ignore_inputs_for_rendering() const override; + +protected: + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + +private: + TrackList *get_track_list() const; + + Sequence *sequence_; +}; + +} + +#endif // OAK_MULTICAMNODE_H diff --git a/src/node/src/input/time/CMakeLists.txt b/src/node/src/input/time/CMakeLists.txt new file mode 100644 index 000000000..dcc559e66 --- /dev/null +++ b/src/node/src/input/time/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/input/time/timeinput.h + node/input/time/timeinput.cpp + PARENT_SCOPE +) diff --git a/src/node/src/input/time/timeinput.cpp b/src/node/src/input/time/timeinput.cpp new file mode 100644 index 000000000..5db61d7a2 --- /dev/null +++ b/src/node/src/input/time/timeinput.cpp @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timeinput.h" + +namespace olive +{ + +#define super Node + +TimeInput::TimeInput() +{ +} + +std::string TimeInput::name() const +{ + return "Time"; +} + +std::string TimeInput::id() const +{ + return "org.olivevideoeditor.Olive.time"; +} + +std::vector TimeInput::category() const +{ + return { k_category_time }; +} + +std::string TimeInput::description() const +{ + return "Generates the time (in seconds) at this frame."; +} + +void TimeInput::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + table->push(NodeValue::k_float, globals.time().in().to_double(), this, false, + "time"); +} + +} diff --git a/src/node/src/input/time/timeinput.h b/src/node/src/input/time/timeinput.h new file mode 100644 index 000000000..c86cbca33 --- /dev/null +++ b/src/node/src/input/time/timeinput.h @@ -0,0 +1,47 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TIMEINPUT_H +#define OAK_TIMEINPUT_H + +#include "node.h" + +namespace olive +{ + +class TimeInput : public Node { +public: + TimeInput(); + + NODE_DEFAULT_FUNCTIONS(TimeInput) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; +}; + +} + +#endif // OAK_TIMEINPUT_H diff --git a/src/node/src/input/value/CMakeLists.txt b/src/node/src/input/value/CMakeLists.txt new file mode 100644 index 000000000..16cdc21ca --- /dev/null +++ b/src/node/src/input/value/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/input/value/valuenode.h + node/input/value/valuenode.cpp + PARENT_SCOPE +) diff --git a/src/node/src/input/value/valuenode.cpp b/src/node/src/input/value/valuenode.cpp new file mode 100644 index 000000000..280811c31 --- /dev/null +++ b/src/node/src/input/value/valuenode.cpp @@ -0,0 +1,82 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "valuenode.h" + +namespace olive +{ + +const std::string ValueNode::k_type_input = "type_in"; +const std::string ValueNode::k_value_input = "value_in"; +const std::vector ValueNode::k_supported_types = { + NodeValue::k_float, NodeValue::k_int, NodeValue::k_rational, + NodeValue::k_vec2, NodeValue::k_vec3, NodeValue::k_vec4, + NodeValue::k_color, NodeValue::k_text, NodeValue::k_matrix, + NodeValue::k_font, NodeValue::k_boolean, +}; + +#define super Node + +ValueNode::ValueNode() +{ + add_input(k_type_input, NodeValue::k_combo, 0, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_value_input, k_supported_types.front(), Variant(), + InputFlags(k_input_flag_not_connectable)); +} + +void ValueNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_type_input, "Type"); + set_input_name(k_value_input, "Value"); + + StringList type_names; + type_names.reserve(k_supported_types.size()); + for (NodeValue::Type type : k_supported_types) { + type_names.push_back(NodeValue::get_pretty_data_type_name(type)); + } + set_combo_box_strings(k_type_input, type_names); +} + +void ValueNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + (void) globals; + + // Ensure value is pushed onto the table + table->push(value.at(k_value_input)); +} + +void ValueNode::InputValueChangedEvent(const std::string &input, int element) +{ + if (input == k_type_input) { + set_input_data_type( + k_value_input, + k_supported_types.at(get_standard_value(k_type_input).to_int())); + } + + super::InputValueChangedEvent(input, element); +} + +} diff --git a/src/node/src/input/value/valuenode.h b/src/node/src/input/value/valuenode.h new file mode 100644 index 000000000..fc0f6848b --- /dev/null +++ b/src/node/src/input/value/valuenode.h @@ -0,0 +1,74 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_VALUENODE_H +#define OAK_VALUENODE_H + +#include "node.h" + +namespace olive +{ + +class ValueNode : public Node { +public: + ValueNode(); + + NODE_DEFAULT_FUNCTIONS(ValueNode) + + virtual std::string name() const override + { + return "Value"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.value"; + } + + virtual std::vector category() const override + { + return { k_category_generator }; + } + + virtual std::string description() const override + { + return "Create a single value that can be connected to various other inputs."; + } + + static const std::string k_type_input; + static const std::string k_value_input; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + +protected: + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + +private: + static const std::vector k_supported_types; +}; + +} + +#endif // OAK_VALUENODE_H diff --git a/src/node/src/inputdragger.cpp b/src/node/src/inputdragger.cpp new file mode 100644 index 000000000..507e32fc9 --- /dev/null +++ b/src/node/src/inputdragger.cpp @@ -0,0 +1,163 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "inputdragger.h" + +#include + +#include "node.h" +#include "nodeundo.h" + +namespace olive +{ + +int NodeInputDragger::input_being_dragged = 0; + +NodeInputDragger::NodeInputDragger() +{ +} + +bool NodeInputDragger::is_started() const +{ + return input_.is_valid(); +} + +void NodeInputDragger::start(const NodeKeyframeTrackReference &input, + const Rational &time, + bool create_key_on_all_tracks) +{ + assert(!is_started()); + + // Set up new drag + input_ = input; + time_ = time; + + Node *node = input_.input().node(); + + // Cache current value + start_value_ = node->get_split_value_at_time_on_track(input_, time); + end_value_ = start_value_; + + // Determine whether we are creating a keyframe or not + if (input_.input().is_keyframing()) { + dragging_key_ = node->get_keyframe_at_time_on_track(input_, time); + + if (!dragging_key_) { + dragging_key_ = new NodeKeyframe( + time, start_value_, + node->get_best_keyframe_type_for_time_on_track(input_, time), + input_.track(), input_.input().element(), + input_.input().input(), node); + created_keys_.push_back(dragging_key_); + + if (create_key_on_all_tracks) { + int nb_tracks = NodeValue::get_number_of_keyframe_tracks( + input.input().node()->get_input_data_type( + input.input().input())); + for (int i = 0; i < nb_tracks; i++) { + if (i != input.track()) { + NodeKeyframeTrackReference this_ref(input.input(), i); + created_keys_.push_back(new NodeKeyframe( + time, + node->get_split_value_at_time_on_track(this_ref, time), + node->get_best_keyframe_type_for_time_on_track(this_ref, + time), + i, input.input().element(), input.input().input(), + node)); + } + } + } + } + } + + input_being_dragged++; +} + +void NodeInputDragger::drag(Variant value) +{ + assert(is_started()); + + Node *node = input_.input().node(); + const std::string &input = input_.input().input(); + + if (node->has_input_property(input, "min")) { + // Assumes the value is a double of some kind + double min = + node->get_input_property(input, "min").to_double(); + double v = value.to_double(); + if (v < min) { + value = min; + } + } + + if (node->has_input_property(input, "max")) { + double max = + node->get_input_property(input, "max").to_double(); + double v = value.to_double(); + if (v > max) { + value = max; + } + } + + end_value_ = value; + + //input_->blockSignals(true); + + if (input_.input().is_keyframing()) { + dragging_key_->set_value(value); + } else { + node->set_split_standard_value_on_track(input_, value); + } + + //input_->blockSignals(false); +} + +void NodeInputDragger::end(MultiUndoCommand *command) +{ + if (!is_started()) { + return; + } + + input_being_dragged--; + + if (input_.input().node()->is_input_keyframing(input_.input())) { + for (int i = 0; i < int(created_keys_.size()); i++) { + // We created a keyframe in this process + command->add_child(new NodeParamInsertKeyframeCommand( + input_.input().node(), created_keys_.at(i))); + } + + // We just set a keyframe's value + // We do this even when inserting a keyframe because we don't actually perform an insert in this undo command + // so this will ensure the ValueChanged() signal is sent correctly + command->add_child(new NodeParamSetKeyframeValueCommand( + dragging_key_, end_value_, start_value_)); + } else { + // We just set the standard value + command->add_child(new NodeParamSetStandardValueCommand( + input_, end_value_, start_value_)); + } + + input_.reset(); + created_keys_.clear(); +} + +} diff --git a/src/node/src/inputdragger.h b/src/node/src/inputdragger.h new file mode 100644 index 000000000..f813df041 --- /dev/null +++ b/src/node/src/inputdragger.h @@ -0,0 +1,84 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEINPUTDRAGGER_H +#define OAK_NODEINPUTDRAGGER_H + +#include + +#include "keyframe.h" +#include "param.h" +#include "undocommand.h" + +namespace olive +{ + +class NodeInputDragger { +public: + NodeInputDragger(); + + bool is_started() const; + + void start(const NodeKeyframeTrackReference &input, const Rational &time, + bool create_key_on_all_tracks = true); + + void drag(Variant value); + + void end(MultiUndoCommand *command); + + static bool is_input_being_dragged() + { + return input_being_dragged; + } + + const Variant &get_start_value() const + { + return start_value_; + } + + const NodeKeyframeTrackReference &get_input() const + { + return input_; + } + + const Rational &get_time() const + { + return time_; + } + +private: + NodeKeyframeTrackReference input_; + + Rational time_; + + Variant start_value_; + + Variant end_value_; + + NodeKeyframe *dragging_key_; + std::vector created_keys_; + + static int input_being_dragged; +}; + +} + +#endif // OAK_NODEINPUTDRAGGER_H diff --git a/src/node/src/inputimmediate.cpp b/src/node/src/inputimmediate.cpp new file mode 100644 index 000000000..8c9fc15f1 --- /dev/null +++ b/src/node/src/inputimmediate.cpp @@ -0,0 +1,313 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "inputimmediate.h" + +#include +#include + +#include "node.h" + +namespace olive +{ + +NodeInputImmediate::NodeInputImmediate(NodeValue::Type type, + const SplitValue &default_val) + : default_value_(default_val) + , keyframing_(false) +{ + set_data_type(type); +} + +void NodeInputImmediate::set_standard_value_on_track(const Variant &value, + int track) +{ + standard_value_[track] = value; +} + +void NodeInputImmediate::set_split_standard_value(const SplitValue &value) +{ + for (int i = 0; i < int(value.size()) && i < int(standard_value_.size()); + i++) { + standard_value_[i] = value[i]; + } +} + +std::vector +NodeInputImmediate::get_keyframe_at_time(const Rational &time) const +{ + std::vector keys; + + for (int i = 0; i < int(keyframe_tracks_.size()); i++) { + NodeKeyframe *key_at_time = get_keyframe_at_time_on_track(time, i); + + if (key_at_time) { + keys.push_back(key_at_time); + } + } + + return keys; +} + +NodeKeyframe * +NodeInputImmediate::get_keyframe_at_time_on_track(const Rational &time, + int track) const +{ + if (!is_using_standard_value(track)) { + for (NodeKeyframe *key : keyframe_tracks_.at(track)) { + if (key->time() == time) { + return key; + } + } + } + + return nullptr; +} + +NodeKeyframe * +NodeInputImmediate::get_closest_keyframe_to_time_on_track(const Rational &time, + int track) const +{ + if (is_using_standard_value(track)) { + return nullptr; + } + + const NodeKeyframeTrack &key_track = keyframe_tracks_.at(track); + + if (time <= key_track.front()->time()) { + return key_track.front(); + } + + if (time >= key_track.back()->time()) { + return key_track.back(); + } + + for (int i = 1; i < int(key_track.size()); i++) { + NodeKeyframe *prev_key = key_track.at(i - 1); + NodeKeyframe *next_key = key_track.at(i); + + if (prev_key->time() <= time && next_key->time() >= time) { + // Return whichever is closer + Rational prev_diff = time - prev_key->time(); + Rational next_diff = next_key->time() - time; + + if (next_diff < prev_diff) { + return next_key; + } else { + return prev_key; + } + } + } + + return nullptr; +} + +NodeKeyframe * +NodeInputImmediate::get_closest_keyframe_before_time(const Rational &time) const +{ + NodeKeyframe *key = nullptr; + + for (const NodeKeyframeTrack &track : keyframe_tracks_) { + for (NodeKeyframe *k : track) { + if (k->time() >= time) { + break; + } else if (!key || k->time() > key->time()) { + key = k; + } + } + } + + return key; +} + +NodeKeyframe * +NodeInputImmediate::get_closest_keyframe_after_time(const Rational &time) const +{ + NodeKeyframe *key = nullptr; + + for (const NodeKeyframeTrack &track : keyframe_tracks_) { + for (int i = int(track.size()) - 1; i >= 0; i--) { + NodeKeyframe *k = track.at(i); + + if (k->time() <= time) { + break; + } else if (!key || k->time() < key->time()) { + key = k; + } + } + } + + return key; +} + +NodeKeyframe::Type +NodeInputImmediate::get_best_keyframe_type_for_time(const Rational &time, + int track) const +{ + NodeKeyframe *closest_key = + get_closest_keyframe_to_time_on_track(time, track); + + if (closest_key) { + return closest_key->type(); + } + + return NodeKeyframe::k_default_type; +} + +bool NodeInputImmediate::has_keyframe_at_time(const Rational &time) const +{ + if (!is_keyframing()) { + return false; + } + + // Loop through keyframes to see if any match + for (const NodeKeyframeTrack &track : keyframe_tracks_) { + for (NodeKeyframe *key : track) { + if (key->time() == time) { + return true; + } + } + } + + // None match + return false; +} + +void NodeInputImmediate::set_data_type(NodeValue::Type type) +{ + int track_size = NodeValue::get_number_of_keyframe_tracks(type); + + keyframe_tracks_.resize(track_size); + standard_value_.resize(track_size); + + set_split_standard_value(default_value_); +} + +NodeKeyframe *NodeInputImmediate::get_earliest_keyframe() const +{ + NodeKeyframe *earliest = nullptr; + + for (const NodeKeyframeTrack &track : keyframe_tracks_) { + if (!track.empty()) { + NodeKeyframe *earliest_in_track = track.front(); + + if (!earliest || earliest_in_track->time() < earliest->time()) { + earliest = earliest_in_track; + } + } + } + + return earliest; +} + +NodeKeyframe *NodeInputImmediate::get_latest_keyframe() const +{ + NodeKeyframe *latest = nullptr; + + for (const NodeKeyframeTrack &track : keyframe_tracks_) { + if (!track.empty()) { + NodeKeyframe *latest_in_track = track.back(); + + if (!latest || latest_in_track->time() > latest->time()) { + latest = latest_in_track; + } + } + } + + return latest; +} + +void NodeInputImmediate::insert_keyframe(NodeKeyframe *key) +{ + NodeKeyframeTrack &key_track = keyframe_tracks_[key->track()]; + + int insert_index = int(key_track.size()); + + for (int i = 0; i < int(key_track.size()); i++) { + NodeKeyframe *compare = key_track.at(i); + + // Ensure we aren't trying to insert two keyframes at the same time + assert(compare->time() != key->time()); + + if (compare->time() > key->time()) { + insert_index = i; + break; + } + } + + key_track.insert(key_track.begin() + insert_index, key); + + NodeKeyframe *previous = insert_index > 0 ? key_track.at(insert_index - 1) : + nullptr; + NodeKeyframe *next = insert_index < int(key_track.size()) - 1 ? + key_track.at(insert_index + 1) : + nullptr; + + key->set_previous(previous); + key->set_next(next); + + if (previous) { + previous->set_next(key); + } + + if (next) { + next->set_previous(key); + } +} + +void NodeInputImmediate::remove_keyframe(NodeKeyframe *key) +{ + if (key->previous()) { + key->previous()->set_next(key->next()); + } + + if (key->next()) { + key->next()->set_previous(key->previous()); + } + + key->set_previous(nullptr); + key->set_next(nullptr); + + NodeKeyframeTrack &track = keyframe_tracks_[key->track()]; + track.erase(std::find(track.begin(), track.end(), key)); +} + +void NodeInputImmediate::delete_all_keyframes( + std::vector *reclaimed) +{ + for (NodeKeyframeTrack &track : keyframe_tracks_) { + // Iterate over a copy of the track, since the keyframes are removed + // from it as we go + const NodeKeyframeTrack copy = track; + + for (NodeKeyframe *key : copy) { + remove_keyframe(key); + + if (reclaimed) { + reclaimed->push_back(key); + } else { + delete key; + } + } + } +} + +} diff --git a/src/node/src/inputimmediate.h b/src/node/src/inputimmediate.h new file mode 100644 index 000000000..144bd1387 --- /dev/null +++ b/src/node/src/inputimmediate.h @@ -0,0 +1,194 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEINPUTIMMEDIATE_H +#define OAK_NODEINPUTIMMEDIATE_H + +#include + +#include "keyframe.h" +#include "splitvalue.h" +#include "value.h" +#include "xmlutils.h" + +namespace olive +{ + +class NodeInput; + +class NodeInputImmediate { +public: + NodeInputImmediate(NodeValue::Type type, const SplitValue &default_val); + + /** + * @brief Internal insert function, automatically does an insertion sort based on the keyframe's time + */ + void insert_keyframe(NodeKeyframe *key); + + void remove_keyframe(NodeKeyframe *key); + + /** + * @brief Delete (or hand off) all keyframes + * + * If `reclaimed` is non-null, ownership of the keyframes is transferred + * to that vector instead of deleting them (replaces the old QObject + * reparenting to a memory manager). + */ + void delete_all_keyframes(std::vector *reclaimed = nullptr); + + /** + * @brief Get non-keyframed value split into components (the way it's stored) + */ + const SplitValue &get_split_standard_value() const + { + return standard_value_; + } + + const Variant &get_split_standard_value_on_track(int track) const + { + return standard_value_.at(track); + } + + void set_standard_value_on_track(const Variant &value, int track); + + void set_split_standard_value(const SplitValue &value); + + /** + * @brief Retrieve a list of keyframe objects for all tracks at a given time + * + * List may be empty if this input is not keyframing or has no keyframes at this time. + */ + std::vector get_keyframe_at_time(const Rational &time) const; + + /** + * @brief Retrieve the keyframe object at a given time for a given track + * + * @return + * + * The keyframe object at this time or nullptr if there isn't one or if is_keyframing() is false. + */ + NodeKeyframe *get_keyframe_at_time_on_track(const Rational &time, + int track) const; + + /** + * @brief Gets the closest keyframe to a time + * + * If is_keyframing() is false or keyframes_ is empty, this will return nullptr. + */ + NodeKeyframe *get_closest_keyframe_to_time_on_track(const Rational &time, + int track) const; + + /** + * @brief Get closest keyframe that's before the time on any track + * + * If no keyframe is before this time, returns nullptr. + */ + NodeKeyframe *get_closest_keyframe_before_time(const Rational &time) const; + + /** + * @brief Get closest keyframe that's before the time on any track + * + * If no keyframe is before this time, returns nullptr. + */ + NodeKeyframe *get_closest_keyframe_after_time(const Rational &time) const; + + /** + * @brief A heuristic to determine what type a keyframe should be if it's inserted at a certain time (between keyframes) + */ + NodeKeyframe::Type get_best_keyframe_type_for_time(const Rational &time, + int track) const; + + /** + * @brief Return list of keyframes in this parameter + */ + const std::vector &keyframe_tracks() const + { + return keyframe_tracks_; + } + + /** + * @brief Return whether keyframing is enabled on this input or not + */ + bool is_keyframing() const + { + return keyframing_; + } + + /** + * @brief Set whether keyframing is enabled on this input or not + */ + void set_is_keyframing(bool k) + { + keyframing_ = k; + } + + /** + * @brief Gets the earliest keyframe on any track + */ + NodeKeyframe *get_earliest_keyframe() const; + + /** + * @brief Gets the latest keyframe on any track + */ + NodeKeyframe *get_latest_keyframe() const; + + /** + * @brief Return whether a keyframe exists at this time + * + * If is_keyframing() is false, this will always return false. This checks all tracks and will return true if *any* + * track has a keyframe. + */ + bool has_keyframe_at_time(const Rational &time) const; + + bool is_using_standard_value(int track) const + { + return (!is_keyframing() || keyframe_tracks_.at(track).empty()); + } + + void set_data_type(NodeValue::Type type); + +private: + /** + * @brief Non-keyframed value + */ + SplitValue standard_value_; + + /** + * @brief Default value + */ + SplitValue default_value_; + + /** + * @brief Internal keyframe array + * + * If keyframing is enabled, this data is used instead of standard_value. + */ + std::vector keyframe_tracks_; + + /** + * @brief Internal keyframing enabled setting + */ + bool keyframing_; +}; + +} + +#endif // OAK_NODEINPUTIMMEDIATE_H diff --git a/src/node/src/keyframe.cpp b/src/node/src/keyframe.cpp new file mode 100644 index 000000000..8e7b1211a --- /dev/null +++ b/src/node/src/keyframe.cpp @@ -0,0 +1,298 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "keyframe.h" + +#include +#include +#include + +#include "node.h" +#include "xmlutils.h" + +namespace olive +{ + +const NodeKeyframe::Type NodeKeyframe::k_default_type = k_linear; + +static std::string number_to_string(double d) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", d); + return std::string(buf); +} + +NodeKeyframe::NodeKeyframe(const Rational &time, const Variant &value, + Type type, int track, int element, + const std::string &input, Node *parent) + : time_(time) + , value_(value) + , type_(type) + , bezier_control_in_(PointF(0.0, 0.0)) + , bezier_control_out_(PointF(0.0, 0.0)) + , input_(input) + , track_(track) + , element_(element) + , parent_(parent) + , previous_(nullptr) + , next_(nullptr) +{ +} + +NodeKeyframe::NodeKeyframe() + : type_(NodeKeyframe::k_linear) + , bezier_control_in_(PointF(0.0, 0.0)) + , bezier_control_out_(PointF(0.0, 0.0)) + , track_(-1) + , element_(-1) + , parent_(nullptr) + , previous_(nullptr) + , next_(nullptr) +{ +} + +NodeKeyframe::~NodeKeyframe() +{ +} + +NodeKeyframe *NodeKeyframe::copy(int element, Node *parent) const +{ + NodeKeyframe *copy = + new NodeKeyframe(time_, value_, type_, track_, element, input_, parent); + copy->bezier_control_in_ = bezier_control_in_; + copy->bezier_control_out_ = bezier_control_out_; + return copy; +} + +NodeKeyframe *NodeKeyframe::copy(Node *parent) const +{ + return copy(element_, parent); +} + +Node *NodeKeyframe::parent() const +{ + return parent_; +} + +const Rational &NodeKeyframe::time() const +{ + return time_; +} + +void NodeKeyframe::set_time(const Rational &time) +{ + time_ = time; +} + +const Variant &NodeKeyframe::value() const +{ + return value_; +} + +void NodeKeyframe::set_value(const Variant &value) +{ + value_ = value; +} + +const NodeKeyframe::Type &NodeKeyframe::type() const +{ + return type_; +} + +void NodeKeyframe::set_type(const NodeKeyframe::Type &type) +{ + if (type_ != type) { + set_type_no_bezier_adj(type); + + if (type_ == k_bezier) { + // Set some sane defaults if this keyframe existed in the track and was just changed + if (bezier_control_in_.is_null()) { + if (previous_) { + // Set the in point to be half way between + set_bezier_control_in( + PointF((previous_->time().to_double() - + this->time().to_double()) * + 0.5, + 0.0)); + } else { + set_bezier_control_in(PointF(-1.0, 0.0)); + } + } + + if (bezier_control_out_.is_null()) { + if (next_) { + set_bezier_control_out(PointF( + (next_->time().to_double() - this->time().to_double()) * + 0.5, + 0.0)); + } else { + set_bezier_control_out(PointF(1.0, 0.0)); + } + } + } + } +} + +void NodeKeyframe::set_type_no_bezier_adj(const Type &type) +{ + type_ = type; +} + +const PointF &NodeKeyframe::bezier_control_in() const +{ + return bezier_control_in_; +} + +void NodeKeyframe::set_bezier_control_in(const PointF &control) +{ + bezier_control_in_ = control; +} + +const PointF &NodeKeyframe::bezier_control_out() const +{ + return bezier_control_out_; +} + +void NodeKeyframe::set_bezier_control_out(const PointF &control) +{ + bezier_control_out_ = control; +} + +PointF NodeKeyframe::valid_bezier_control_in() const +{ + double t = time().to_double(); + double adjusted_x = t + bezier_control_in_.x(); + + if (previous_) { + // Limit to the point of that keyframe + adjusted_x = std::max(adjusted_x, previous_->time().to_double()); + } + + return PointF(adjusted_x - t, bezier_control_in_.y()); +} + +PointF NodeKeyframe::valid_bezier_control_out() const +{ + double t = time().to_double(); + double adjusted_x = t + bezier_control_out_.x(); + + if (next_) { + // Limit to the point of that keyframe + adjusted_x = std::min(adjusted_x, next_->time().to_double()); + } + + return PointF(adjusted_x - t, bezier_control_out_.y()); +} + +const PointF &NodeKeyframe::bezier_control(NodeKeyframe::BezierType type) const +{ + if (type == k_in_handle) { + return bezier_control_in(); + } else { + return bezier_control_out(); + } +} + +void NodeKeyframe::set_bezier_control(NodeKeyframe::BezierType type, + const PointF &control) +{ + if (type == k_in_handle) { + set_bezier_control_in(control); + } else { + set_bezier_control_out(control); + } +} + +NodeKeyframe::BezierType +NodeKeyframe::get_opposing_bezier_type(NodeKeyframe::BezierType type) +{ + if (type == k_in_handle) { + return k_out_handle; + } else { + return k_in_handle; + } +} + +bool NodeKeyframe::has_sibling_at_time(const Rational &t) const +{ + NodeKeyframe *k = + parent()->get_keyframe_at_time_on_track(input(), t, track(), element()); + return k && k != this; +} + +bool NodeKeyframe::load(XmlStreamReader *reader, NodeValue::Type data_type) +{ + std::string key_input; + PointF key_in_handle; + PointF key_out_handle; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "input") { + key_input = attr.value; + } else if (attr.name == "time") { + this->set_time(Rational::from_string(attr.value)); + } else if (attr.name == "type") { + this->set_type_no_bezier_adj(static_cast( + strtol(attr.value.c_str(), nullptr, 10))); + } else if (attr.name == "inhandlex") { + key_in_handle.set_x(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "inhandley") { + key_in_handle.set_y(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandlex") { + key_out_handle.set_x(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandley") { + key_out_handle.set_y(strtod(attr.value.c_str(), nullptr)); + } + } + + this->set_value( + NodeValue::string_to_value(data_type, reader->read_element_text(), true)); + + if (!key_input.empty()) { + this->set_input(key_input); + } + + this->set_bezier_control_in(key_in_handle); + this->set_bezier_control_out(key_out_handle); + + return true; +} + +void NodeKeyframe::save(XmlStreamWriter *writer, + NodeValue::Type data_type) const +{ + writer->write_attribute("input", this->input()); + writer->write_attribute("time", this->time().to_string()); + writer->write_attribute("type", std::to_string(this->type())); + writer->write_attribute("inhandlex", + number_to_string(this->bezier_control_in().x())); + writer->write_attribute("inhandley", + number_to_string(this->bezier_control_in().y())); + writer->write_attribute("outhandlex", + number_to_string(this->bezier_control_out().x())); + writer->write_attribute("outhandley", + number_to_string(this->bezier_control_out().y())); + + writer->write_characters( + NodeValue::value_to_string(data_type, this->value(), true)); +} + +} diff --git a/src/node/src/keyframe.h b/src/node/src/keyframe.h new file mode 100644 index 000000000..177db9033 --- /dev/null +++ b/src/node/src/keyframe.h @@ -0,0 +1,225 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEKEYFRAME_H +#define OAK_NODEKEYFRAME_H + +#include +#include +#include + +#include "param.h" + +namespace olive +{ + +class Node; +class XmlStreamReader; +class XmlStreamWriter; + +/** + * @brief A point of data to be used at a certain time and interpolated with other data + */ +class NodeKeyframe { +public: + /** + * @brief Methods of interpolation to use with this keyframe + */ + enum Type { k_invalid = -1, k_linear, k_hold, k_bezier }; + + /** + * @brief The two types of bezier handles that are available on bezier keyframes + */ + enum BezierType { k_in_handle, k_out_handle }; + + static const Type k_default_type; + + /** + * @brief NodeKeyframe Constructor + */ + NodeKeyframe(const Rational &time, const Variant &value, Type type, + int track, int element, const std::string &input, + Node *parent = nullptr); + NodeKeyframe(); + + virtual ~NodeKeyframe(); + + NodeKeyframe *copy(int element, Node *parent = nullptr) const; + NodeKeyframe *copy(Node *parent = nullptr) const; + + Node *parent() const; + + /** + * @brief Set the Node this keyframe belongs to. + * + * Called by Node::add_keyframe()/remove_keyframe(). Replaces the former + * QObject parent-child mechanism. + */ + void set_parent(Node *p) + { + parent_ = p; + } + + const std::string &input() const + { + return input_; + } + void set_input(const std::string &input) + { + input_ = input; + } + + NodeKeyframeTrackReference key_track_ref() const + { + return NodeKeyframeTrackReference( + NodeInput(parent(), input(), element()), track()); + } + + /** + * @brief The time this keyframe is set at + */ + const Rational &time() const; + void set_time(const Rational &time); + + /** + * @brief The value of this keyframe (i.e. the value to use at this keyframe's time) + */ + const Variant &value() const; + void set_value(const Variant &value); + + /** + * @brief The method of interpolation to use with this keyframe + */ + const Type &type() const; + void set_type(const Type &type); + void set_type_no_bezier_adj(const Type &type); + + /** + * @brief For bezier interpolation, the control point leading into this keyframe + */ + const PointF &bezier_control_in() const; + void set_bezier_control_in(const PointF &control); + + /** + * @brief For bezier interpolation, the control point leading out of this keyframe + */ + const PointF &bezier_control_out() const; + void set_bezier_control_out(const PointF &control); + + /** + * @brief Returns a known good bezier that should be used in actual animation + * + * While users can move the bezier controls wherever they want, we have to limit their usage + * internally to prevent a situation where the animation overlaps (i.e. there can only be one Y + * value for any given X in the bezier line). This returns a value that is known good. + */ + PointF valid_bezier_control_in() const; + PointF valid_bezier_control_out() const; + + /** + * @brief Convenience functions for retrieving/setting bezier handle information with a BezierType + */ + const PointF &bezier_control(BezierType type) const; + void set_bezier_control(BezierType type, const PointF &control); + + /** + * @brief The track that this keyframe belongs to + * + * For the majority of keyfreames, this will be 0, but for some types, such as kVec2, this will be 0 for X keyframes + * and 1 for Y keyframes, etc. + */ + int track() const + { + return track_; + } + void set_track(int t) + { + track_ = t; + } + + int element() const + { + return element_; + } + void set_element(int e) + { + element_ = e; + } + + /** + * @brief Convenience function for getting the opposite handle type (e.g. kInHandle <-> kOutHandle) + */ + static BezierType get_opposing_bezier_type(BezierType type); + + NodeKeyframe *previous() const + { + return previous_; + } + + void set_previous(NodeKeyframe *keyframe) + { + previous_ = keyframe; + } + + NodeKeyframe *next() const + { + return next_; + } + + void set_next(NodeKeyframe *keyframe) + { + next_ = keyframe; + } + + bool has_sibling_at_time(const Rational &t) const; + + bool load(XmlStreamReader *reader, NodeValue::Type data_type); + void save(XmlStreamWriter *writer, NodeValue::Type data_type) const; + +private: + Rational time_; + + Variant value_; + + Type type_; + + PointF bezier_control_in_; + + PointF bezier_control_out_; + + std::string input_; + + int track_; + + int element_; + + Node *parent_; + + NodeKeyframe *previous_; + + NodeKeyframe *next_; +}; + +using NodeKeyframeTrack = std::vector; + +} + +#endif // OAK_NODEKEYFRAME_H diff --git a/src/node/src/keying/CMakeLists.txt b/src/node/src/keying/CMakeLists.txt new file mode 100644 index 000000000..dfb8f2fda --- /dev/null +++ b/src/node/src/keying/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(chromakey) +add_subdirectory(colordifferencekey) +add_subdirectory(despill) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/keying/chromakey/CMakeLists.txt b/src/node/src/keying/chromakey/CMakeLists.txt new file mode 100644 index 000000000..6107bd105 --- /dev/null +++ b/src/node/src/keying/chromakey/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/keying/chromakey/chromakey.h + node/keying/chromakey/chromakey.cpp + PARENT_SCOPE +) \ No newline at end of file diff --git a/src/node/src/keying/chromakey/chromakey.cpp b/src/node/src/keying/chromakey/chromakey.cpp new file mode 100644 index 000000000..98e1310f3 --- /dev/null +++ b/src/node/src/keying/chromakey/chromakey.cpp @@ -0,0 +1,182 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#include "chromakey.h" + +#include "color/colormanager/colormanager.h" +#include "render/colorprocessor.h" + +namespace olive +{ + +#define super OCIOBaseNode + +const std::string ChromaKeyNode::k_color_input = "color_key"; +const std::string ChromaKeyNode::k_mask_only_input = "mask_only_in"; +const std::string ChromaKeyNode::k_invert_input = "invert_in"; +const std::string ChromaKeyNode::k_upper_tolerance_input = "upper_tolerance_in"; +const std::string ChromaKeyNode::k_lower_tolerance_input = "lower_tolerance_in"; +const std::string ChromaKeyNode::k_garbage_matte_input = "garbage_in"; +const std::string ChromaKeyNode::k_core_matte_input = "core_in"; +const std::string ChromaKeyNode::k_shadows_input = "shadows_in"; +const std::string ChromaKeyNode::k_highlights_input = "highlights_in"; + +ChromaKeyNode::ChromaKeyNode() +{ + add_input(k_color_input, NodeValue::k_color, + Variant::from_value(Color(0.0f, 1.0f, 0.0f, 1.0f))); + + add_input(k_lower_tolerance_input, NodeValue::k_float, 5.0); + set_input_property(k_lower_tolerance_input, "min", 0.0); + set_input_property(k_lower_tolerance_input, "base", 0.1); + + add_input(k_upper_tolerance_input, NodeValue::k_float, 25.0); + set_input_property(k_upper_tolerance_input, "base", 0.1); + + // FIXME: Temporarily disabled. This will break if "lower tolerance" is keyframed or connected to + // something and there's currently no solution to remedy that. If there is in the future, + // we can look into re-enabling this. + //SetInputProperty(kUpperToleranceInput, QStringLiteral("min"), GetStandardValue(kLowerToleranceInput).toDouble()); + + add_input(k_garbage_matte_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_core_matte_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_highlights_input, NodeValue::k_float, 100.0f); + set_input_property(k_highlights_input, "min", 0.0); + set_input_property(k_highlights_input, "base", 0.1); + + add_input(k_shadows_input, NodeValue::k_float, 100.0f); + set_input_property(k_shadows_input, "min", 0.0); + set_input_property(k_shadows_input, "base", 0.1); + + add_input(k_invert_input, NodeValue::k_boolean, false); + + add_input(k_mask_only_input, NodeValue::k_boolean, false); +} + +std::string ChromaKeyNode::name() const +{ + return "Chroma Key"; +} + +std::string ChromaKeyNode::id() const +{ + return "org.olivevideoeditor.Olive.chromakey"; +} + +std::vector ChromaKeyNode::category() const +{ + return { k_category_keying }; +} + +std::string ChromaKeyNode::description() const +{ + return "A simple color key based on the distance from the chroma of a selected color."; +} + +void ChromaKeyNode::retranslate() +{ + super::retranslate(); + set_input_name(k_texture_input, "Input"); + set_input_name(k_garbage_matte_input, "Garbage Matte"); + set_input_name(k_core_matte_input, "Core Matte"); + set_input_name(k_color_input, "Key Color"); + set_input_name(k_shadows_input, "Shadows"); + set_input_name(k_highlights_input, "Highlights"); + set_input_name(k_upper_tolerance_input, "Upper Tolerance"); + set_input_name(k_lower_tolerance_input, "Lower Tolerance"); + set_input_name(k_invert_input, "Invert Mask"); + set_input_name(k_mask_only_input, "Show Mask Only"); +} + +void ChromaKeyNode::InputValueChangedEvent(const std::string &input, int element) +{ + (void) element; + if (input == k_lower_tolerance_input) { + // FIXME: Temporarily disabled. This will break if "lower tolerance" is keyframed or connected to + // something and there's currently no solution to remedy that. If there is in the future, + // we can look into re-enabling this. + //SetInputProperty(kUpperToleranceInput, QStringLiteral("min"), GetStandardValue(kLowerToleranceInput).toDouble()); + } + + generate_processor(); +} + +ShaderCode ChromaKeyNode::get_shader_code(const ShaderRequest &request) const +{ + std::string frag = + FileFunctions::read_file_as_string(":/shaders/chromakey.frag"); + // QString::arg() equivalent: replace every "%1" marker with the stub + std::string::size_type pos = 0; + while ((pos = frag.find("%1", pos)) != std::string::npos) { + frag.replace(pos, 2, request.stub); + pos += request.stub.size(); + } + return ShaderCode(frag); +} + +void ChromaKeyNode::generate_processor() +{ + if (manager()) { + try { + ColorTransform transform("cie_xyz_d65_interchange"); + set_processor(ColorProcessor::create( + manager(), manager()->get_reference_color_space(), transform)); + } catch (const ocio::Exception &e) { + std::cerr << std::endl << e.what() << std::endl; + } + } +} + +void ChromaKeyNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + if (processor()) { + ColorTransformJob job(value); + + job.set_color_processor(processor()); + job.set_input_texture(value.at(k_texture_input)); + job.set_needs_custom_shader(this); + job.set_function_name("SceneLinearToCIEXYZ_d65"); + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } + } +} + +void ChromaKeyNode::config_changed() +{ + generate_processor(); +} + +std::string ChromaKeyNode::get_input_id_for_legacy_id(const std::string &id) const +{ + // Older project files used the misspelled "tolerence" input IDs + if (id == "upper_tolerence_in") { + return k_upper_tolerance_input; + } + if (id == "lower_tolerence_in") { + return k_lower_tolerance_input; + } + + return super::get_input_id_for_legacy_id(id); +} + +} // namespace olive diff --git a/src/node/src/keying/chromakey/chromakey.h b/src/node/src/keying/chromakey/chromakey.h new file mode 100644 index 000000000..a058176fc --- /dev/null +++ b/src/node/src/keying/chromakey/chromakey.h @@ -0,0 +1,69 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef OAK_CHROMAKEYNODE_H +#define OAK_CHROMAKEYNODE_H + +#include "color/ociobase/ociobase.h" + +namespace olive +{ + +class ChromaKeyNode : public OCIOBaseNode { +public: + ChromaKeyNode(); + + NODE_DEFAULT_FUNCTIONS(ChromaKeyNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void config_changed() override; + + // Maps the misspelled tolerance input IDs from old project files onto the + // corrected ones + virtual std::string + get_input_id_for_legacy_id(const std::string &id) const override; + + static const std::string k_color_input; + static const std::string k_invert_input; + static const std::string k_mask_only_input; + static const std::string k_upper_tolerance_input; + static const std::string k_lower_tolerance_input; + static const std::string k_garbage_matte_input; + static const std::string k_core_matte_input; + static const std::string k_shadows_input; + static const std::string k_highlights_input; + +private: + void generate_processor(); +}; + +} // namespace olive + +#endif // OAK_CHROMAKEYNODE_H diff --git a/src/node/src/keying/colordifferencekey/CMakeLists.txt b/src/node/src/keying/colordifferencekey/CMakeLists.txt new file mode 100644 index 000000000..0e2e48980 --- /dev/null +++ b/src/node/src/keying/colordifferencekey/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/keying/colordifferencekey/colordifferencekey.h + node/keying/colordifferencekey/colordifferencekey.cpp + PARENT_SCOPE +) diff --git a/src/node/src/keying/colordifferencekey/colordifferencekey.cpp b/src/node/src/keying/colordifferencekey/colordifferencekey.cpp new file mode 100644 index 000000000..f2df0c2f0 --- /dev/null +++ b/src/node/src/keying/colordifferencekey/colordifferencekey.cpp @@ -0,0 +1,113 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#include "colordifferencekey.h" + +namespace olive +{ + +const std::string ColorDifferenceKeyNode::k_texture_input = "tex_in"; +const std::string ColorDifferenceKeyNode::k_garbage_matte_input = "garbage_in"; +const std::string ColorDifferenceKeyNode::k_core_matte_input = "core_in"; +const std::string ColorDifferenceKeyNode::k_color_input = "color_in"; +const std::string ColorDifferenceKeyNode::k_shadows_input = "shadows_in"; +const std::string ColorDifferenceKeyNode::k_highlights_input = "highlights_in"; +const std::string ColorDifferenceKeyNode::k_mask_only_input = "mask_only_in"; + +#define super Node + +ColorDifferenceKeyNode::ColorDifferenceKeyNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_garbage_matte_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_core_matte_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_color_input, NodeValue::k_combo, 0); + + add_input(k_highlights_input, NodeValue::k_float, 1.0f); + set_input_property(k_highlights_input, "min", 0.0); + set_input_property(k_highlights_input, "base", 0.01); + + add_input(k_shadows_input, NodeValue::k_float, 1.0f); + set_input_property(k_shadows_input, "min", 0.0); + set_input_property(k_shadows_input, "base", 0.01); + + add_input(k_mask_only_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +std::string ColorDifferenceKeyNode::name() const +{ + return "Color Difference Key"; +} + +std::string ColorDifferenceKeyNode::id() const +{ + return "org.olivevideoeditor.Olive.colordifferencekey"; +} + +std::vector ColorDifferenceKeyNode::category() const +{ + return { k_category_keying }; +} + +std::string ColorDifferenceKeyNode::description() const +{ + return "A simple color key based on the distance of one color from other colors."; +} + +void ColorDifferenceKeyNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + set_input_name(k_garbage_matte_input, "Garbage Matte"); + set_input_name(k_core_matte_input, "Core Matte"); + set_input_name(k_color_input, "Key Color"); + set_combo_box_strings(k_color_input, { "Green", "Blue" }); + set_input_name(k_shadows_input, "Shadows"); + set_input_name(k_highlights_input, "Highlights"); + set_input_name(k_mask_only_input, "Show Mask Only"); +} + +ShaderCode +ColorDifferenceKeyNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/colordifferencekey.frag")); +} + +void ColorDifferenceKeyNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + // If there's no texture, no need to run an operation + if (TexturePtr tex = value.at(k_texture_input).to_texture()) { + ShaderJob job; + job.insert(value); + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} + +} // namespace olive diff --git a/src/node/src/keying/colordifferencekey/colordifferencekey.h b/src/node/src/keying/colordifferencekey/colordifferencekey.h new file mode 100644 index 000000000..2083cc97b --- /dev/null +++ b/src/node/src/keying/colordifferencekey/colordifferencekey.h @@ -0,0 +1,54 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef OAK_COLORDIFFERENCEKEYNODE_H +#define OAK_COLORDIFFERENCEKEYNODE_H + +#include "node.h" + +namespace olive +{ + +class ColorDifferenceKeyNode : public Node { +public: + ColorDifferenceKeyNode(); + + NODE_DEFAULT_FUNCTIONS(ColorDifferenceKeyNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_garbage_matte_input; + static const std::string k_core_matte_input; + static const std::string k_color_input; + static const std::string k_shadows_input; + static const std::string k_highlights_input; + static const std::string k_mask_only_input; +}; + +} // namespace olive + +#endif // OAK_COLORDIFFERENCEKEYNODE_H diff --git a/src/node/src/keying/despill/CMakeLists.txt b/src/node/src/keying/despill/CMakeLists.txt new file mode 100644 index 000000000..69141556b --- /dev/null +++ b/src/node/src/keying/despill/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/keying/despill/despill.h + node/keying/despill/despill.cpp + PARENT_SCOPE +) diff --git a/src/node/src/keying/despill/despill.cpp b/src/node/src/keying/despill/despill.cpp new file mode 100644 index 000000000..d5ca0317d --- /dev/null +++ b/src/node/src/keying/despill/despill.cpp @@ -0,0 +1,115 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#include "despill.h" + +#include "project.h" + +namespace olive +{ + +const std::string DespillNode::k_texture_input = "tex_in"; +const std::string DespillNode::k_color_input = "color_in"; +const std::string DespillNode::k_method_input = "method_in"; +const std::string DespillNode::k_preserve_luminance_input = + "preserve_luminance_input"; + +#define super Node + +DespillNode::DespillNode() +{ + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_color_input, NodeValue::k_combo, 0); + + add_input(k_method_input, NodeValue::k_combo, 0); + + add_input(k_preserve_luminance_input, NodeValue::k_boolean, false); + + set_flag(k_video_effect); + set_effect_input(k_texture_input); +} + +std::string DespillNode::name() const +{ + return "Despill"; +} + +std::string DespillNode::id() const +{ + return "org.olivevideoeditor.Olive.despill"; +} + +std::vector DespillNode::category() const +{ + return { k_category_keying }; +} + +std::string DespillNode::description() const +{ + return "Selection of simple despill operations"; +} + +void DespillNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_texture_input, "Input"); + + set_input_name(k_color_input, "Key Color"); + set_combo_box_strings(k_color_input, { "Green", "Blue" }); + + set_input_name(k_method_input, "Method"); + set_combo_box_strings(k_method_input, { "Average", "Double Red Average", + "Double Average", "Limit" }); + + set_input_name(k_preserve_luminance_input, "Preserve Luminance"); +} + +ShaderCode DespillNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/despill.frag")); +} + +void DespillNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + ShaderJob job; + job.insert(value); + + // Set luma coefficients + double luma_coeffs[3] = { 0.0f, 0.0f, 0.0f }; + if (project() && project()->color_manager()) { + project()->color_manager()->get_default_luma_coefs(luma_coeffs); + } else { + luma_coeffs[0] = 0.2126; + luma_coeffs[1] = 0.7152; + luma_coeffs[2] = 0.0722; + } + job.insert("luma_coeffs", + NodeValue(NodeValue::k_vec3, + Vector3D(luma_coeffs[0], luma_coeffs[1], luma_coeffs[2]))); + + // If there's no texture, no need to run an operation + if (TexturePtr tex = job.get(k_texture_input).to_texture()) { + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} + +} // namespace olive diff --git a/src/node/src/keying/despill/despill.h b/src/node/src/keying/despill/despill.h new file mode 100644 index 000000000..502efc7eb --- /dev/null +++ b/src/node/src/keying/despill/despill.h @@ -0,0 +1,52 @@ +/*** + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + Modifications Copyright (C) 2025 mikesolar + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef OAK_DESPILLNODE_H +#define OAK_DESPILLNODE_H + +#include "node.h" +#include "color/colormanager/colormanager.h" + +namespace olive +{ + +class DespillNode : public Node { +public: + DespillNode(); + + NODE_DEFAULT_FUNCTIONS(DespillNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_texture_input; + static const std::string k_color_input; + static const std::string k_method_input; + static const std::string k_preserve_luminance_input; +}; + +} // namespace olive + +#endif // OAK_DESPILLNODE_H diff --git a/src/node/src/math/CMakeLists.txt b/src/node/src/math/CMakeLists.txt new file mode 100644 index 000000000..9018df4d3 --- /dev/null +++ b/src/node/src/math/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(math) +add_subdirectory(merge) +add_subdirectory(trigonometry) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/math/math/CMakeLists.txt b/src/node/src/math/math/CMakeLists.txt new file mode 100644 index 000000000..92981e05e --- /dev/null +++ b/src/node/src/math/math/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/math/math/math.h + node/math/math/math.cpp + node/math/math/mathbase.h + node/math/math/mathbase.cpp + PARENT_SCOPE +) diff --git a/src/node/src/math/math/math.cpp b/src/node/src/math/math/math.cpp new file mode 100644 index 000000000..ce2cf3fc6 --- /dev/null +++ b/src/node/src/math/math/math.cpp @@ -0,0 +1,126 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "math.h" + +namespace olive +{ + +const std::string MathNode::k_method_in = "method_in"; +const std::string MathNode::k_param_a_in = "param_a_in"; +const std::string MathNode::k_param_b_in = "param_b_in"; +const std::string MathNode::k_param_c_in = "param_c_in"; + +#define super MathNodeBase + +MathNode::MathNode() +{ + add_input(k_method_in, NodeValue::k_combo, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_param_a_in, NodeValue::k_float, 0.0); + set_input_property(k_param_a_in, "decimalplaces", 8); + set_input_property(k_param_a_in, "autotrim", true); + + add_input(k_param_b_in, NodeValue::k_float, 0.0); + set_input_property(k_param_b_in, "decimalplaces", 8); + set_input_property(k_param_b_in, "autotrim", true); +} + +std::string MathNode::name() const +{ + // Default to naming after the operation + if (parent()) { + std::string op_name = get_operation_name(get_operation()); + if (!op_name.empty()) { + return op_name; + } + } + + return "Math"; +} + +std::string MathNode::id() const +{ + return "org.olivevideoeditor.Olive.math"; +} + +std::vector MathNode::category() const +{ + return { k_category_math }; +} + +std::string MathNode::description() const +{ + return "Perform a mathematical operation between two values."; +} + +void MathNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_method_in, "Method"); + set_input_name(k_param_a_in, "Value"); + set_input_name(k_param_b_in, "Value"); + + StringList operations = { get_operation_name(k_op_add), + get_operation_name(k_op_subtract), + get_operation_name(k_op_multiply), + get_operation_name(k_op_divide), + get_operation_name(k_op_power) }; + + set_combo_box_strings(k_method_in, operations); +} + +ShaderCode MathNode::get_shader_code(const ShaderRequest &request) const +{ + return get_shader_code_internal(request.id, k_param_a_in, k_param_b_in); +} + +void MathNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Auto-detect what values to operate with + // FIXME: Very inefficient + NodeValueTable at, bt; + at.push(value.at(k_param_a_in)); + bt.push(value.at(k_param_b_in)); + PairingCalculator calc(at, bt); + + // Do nothing if no pairing was found + if (!calc.found_most_likely_pairing()) { + return; + } + + return value_internal(get_operation(), calc.get_most_likely_pairing(), k_param_a_in, + calc.get_most_likely_value_a(), k_param_b_in, + calc.get_most_likely_value_b(), globals, table); +} + +void MathNode::process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const +{ + return process_samples_internal(values, get_operation(), k_param_a_in, k_param_b_in, + input, output, index); +} + +} diff --git a/src/node/src/math/math/math.h b/src/node/src/math/math/math.h new file mode 100644 index 000000000..89686c35f --- /dev/null +++ b/src/node/src/math/math/math.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MATHNODE_H +#define OAK_MATHNODE_H + +#include "mathbase.h" + +namespace olive +{ + +class MathNode : public MathNodeBase { +public: + MathNode(); + + NODE_DEFAULT_FUNCTIONS(MathNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + + Operation get_operation() const + { + return static_cast(get_standard_value(k_method_in).to_int()); + } + + void set_operation(Operation o) + { + set_standard_value(k_method_in, o); + } + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const override; + + static const std::string k_method_in; + static const std::string k_param_a_in; + static const std::string k_param_b_in; + static const std::string k_param_c_in; +}; + +} + +#endif // OAK_MATHNODE_H diff --git a/src/node/src/math/math/mathbase.cpp b/src/node/src/math/math/mathbase.cpp new file mode 100644 index 000000000..5d4ae4847 --- /dev/null +++ b/src/node/src/math/math/mathbase.cpp @@ -0,0 +1,800 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "mathbase.h" + +#include +#include +#include + +#include "distort/transform/transformdistortnode.h" +#include "olive/core/util/stringutils.h" + +namespace olive +{ + +ShaderCode MathNodeBase::get_shader_code_internal(const std::string &shader_id, + const std::string ¶m_a_in, + const std::string ¶m_b_in) const +{ + StringList code_id = core::StringUtils::split(shader_id, '.'); + + Operation op = + static_cast(strtol(code_id.at(0).c_str(), nullptr, 10)); + Pairing pairing = + static_cast(strtol(code_id.at(1).c_str(), nullptr, 10)); + NodeValue::Type type_a = + static_cast(strtol(code_id.at(2).c_str(), nullptr, 10)); + NodeValue::Type type_b = + static_cast(strtol(code_id.at(3).c_str(), nullptr, 10)); + + std::string operation, frag, vert; + + if (pairing == k_pair_texture_matrix && op == k_op_multiply) { + // Override the operation for this operation since we multiply texture COORDS by the matrix rather than + const std::string &tex_in = (type_a == NodeValue::k_texture) ? param_a_in : + param_b_in; + const std::string &mat_in = (type_a == NodeValue::k_texture) ? param_b_in : + param_a_in; + + // No-op frag shader (can we return std::string() instead?) + operation = "texture(" + tex_in + ", ove_texcoord)"; + + vert = "uniform mat4 " + mat_in + + ";\n" + "\n" + "in vec4 a_position;\n" + "in vec2 a_texcoord;\n" + "\n" + "out vec2 ove_texcoord;\n" + "\n" + "void main() {\n" + " gl_Position = " + + mat_in + + " * a_position;\n" + " ove_texcoord = a_texcoord;\n" + "}\n"; + + } else { + const std::string var_a = get_shader_variable_call(param_a_in, type_a); + const std::string var_b = get_shader_variable_call(param_b_in, type_b); + + switch (op) { + case k_op_add: + operation = var_a + " + " + var_b; + break; + case k_op_subtract: + operation = var_a + " - " + var_b; + break; + case k_op_multiply: + operation = var_a + " * " + var_b; + break; + case k_op_divide: + operation = var_a + " / " + var_b; + break; + case k_op_power: + if (pairing == k_pair_texture_number) { + // The "number" in this operation has to be declared a vec4 + if (NodeValue::type_is_numeric(type_a)) { + operation = "pow(" + var_b + ", vec4(" + var_a + "))"; + } else { + operation = "pow(" + var_a + ", vec4(" + var_b + "))"; + } + } else { + operation = "pow(" + var_a + ", " + var_b + ")"; + } + break; + } + } + + frag = "uniform " + get_shader_uniform_type(type_a) + " " + param_a_in + + ";\n" + "uniform " + + get_shader_uniform_type(type_b) + " " + param_b_in + + ";\n" + "\n" + "in vec2 ove_texcoord;\n" + "out vec4 frag_color;\n" + "\n" + "void main(void) {\n" + " vec4 c = " + + operation + + ";\n" + " c.a = clamp(c.a, 0.0, 1.0);\n" // Ensure alpha is between 0.0 and 1.0 + " frag_color = c;\n" + "}\n"; + + return ShaderCode(frag, vert); +} + +std::string +MathNodeBase::get_shader_uniform_type(const olive::NodeValue::Type &type) +{ + switch (type) { + case NodeValue::k_texture: + return "sampler2D"; + case NodeValue::k_color: + return "vec4"; + case NodeValue::k_matrix: + return "mat4"; + default: + return "float"; + } +} + +std::string MathNodeBase::get_shader_variable_call(const std::string &input_id, + const NodeValue::Type &type, + const std::string &coord_op) +{ + if (type == NodeValue::k_texture) { + return "texture(" + input_id + ", ove_texcoord" + coord_op + ")"; + } + + return input_id; +} + +Vector4D MathNodeBase::retrieve_vector(const NodeValue &val) +{ + // Variant doesn't know that Vector*D can convert themselves so we do it here + switch (val.type()) { + case NodeValue::k_vec2: { + Vector2D v = val.to_vec2(); + return Vector4D(v.x(), v.y(), 0.0f, 0.0f); + } + case NodeValue::k_vec3: { + Vector3D v = val.to_vec3(); + return Vector4D(v.x(), v.y(), v.z(), 0.0f); + } + case NodeValue::k_vec4: + default: + return val.to_vec4(); + } +} + +void MathNodeBase::push_vector(NodeValueTable *output, + olive::NodeValue::Type type, const Vector4D &vec) const +{ + switch (type) { + case NodeValue::k_vec2: + output->push(type, Vector2D(vec.x(), vec.y()), this); + break; + case NodeValue::k_vec3: + output->push(type, Vector3D(vec.x(), vec.y(), vec.z()), this); + break; + case NodeValue::k_vec4: + output->push(type, vec, this); + break; + default: + break; + } +} + +std::string MathNodeBase::get_operation_name(Operation o) +{ + switch (o) { + case k_op_add: + return "Add"; + case k_op_subtract: + return "Subtract"; + case k_op_multiply: + return "Multiply"; + case k_op_divide: + return "Divide"; + case k_op_power: + return "Power"; + } + + return std::string(); +} + +void MathNodeBase::perform_all_on_float_buffer(Operation operation, float *a, + float b, int start, int end) +{ + for (int j = start; j < end; j++) { + a[j] = perform_all(operation, a[j], b); + } +} + +#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM) +void MathNodeBase::perform_all_on_float_buffer_sse(Operation operation, float *a, + float b, int start, int end) +{ + int end_divisible_4 = (end / 4) * 4; + + // Load number to multiply by into buffer + __m128 mult = _mm_load1_ps(&b); + + switch (operation) { + case k_op_add: + // Loop all values + for (int j = 0; j < end_divisible_4; j += 4) { + _mm_storeu_ps(a + start + j, + _mm_add_ps(_mm_loadu_ps(a + start + j), mult)); + } + break; + case k_op_subtract: + for (int j = 0; j < end_divisible_4; j += 4) { + _mm_storeu_ps(a + start + j, + _mm_sub_ps(_mm_loadu_ps(a + start + j), mult)); + } + break; + case k_op_multiply: + for (int j = 0; j < end_divisible_4; j += 4) { + _mm_storeu_ps(a + start + j, + _mm_mul_ps(_mm_loadu_ps(a + start + j), mult)); + } + break; + case k_op_divide: + for (int j = 0; j < end_divisible_4; j += 4) { + _mm_storeu_ps(a + start + j, + _mm_div_ps(_mm_loadu_ps(a + start + j), mult)); + } + break; + case k_op_power: + // Fallback for operations we can't support here + end_divisible_4 = 0; + break; + } + + // Handle last 1-3 bytes if necessary, or all bytes if we couldn't + // support this op on SSE + perform_all_on_float_buffer(operation, a, b, end_divisible_4, end); +} +#endif + +void MathNodeBase::value_internal( + Operation operation, Pairing pairing, const std::string ¶m_a_in, + const NodeValue &val_a, const std::string ¶m_b_in, const NodeValue &val_b, + const NodeGlobals &globals, NodeValueTable *output) const +{ + switch (pairing) { + case k_pair_number_number: { + if (val_a.type() == NodeValue::k_rational && + val_b.type() == NodeValue::k_rational && operation != k_op_power) { + // Preserve rationals + output->push( + NodeValue::k_rational, + perform_add_sub_mult_div( + operation, val_a.to_rational(), val_b.to_rational()), + this); + } else { + output->push(NodeValue::k_float, + perform_all(operation, + retrieve_number(val_a), + retrieve_number(val_b)), + this); + } + break; + } + + case k_pair_vec_vec: { + // We convert all vectors to Vector4D just for simplicity and exploit the fact that kVec4 is higher than kVec2 in + // the enum to find the largest data type + Vector4D vec_a = retrieve_vector(val_a); + Vector4D vec_b = retrieve_vector(val_b); + + if (operation == k_op_divide) { + // Lower-dimensional vectors are padded with zeros; dividing the + // padding components would be 0/0 (assert in Qt debug builds, NaN + // otherwise). Force those components to 0/1 so the result is a + // well-defined zero, which is discarded by push_vector anyway. + const NodeValue::Type max_type = std::max(val_a.type(), val_b.type()); + if (max_type == NodeValue::k_vec2) { + vec_a.set_z(0.0f); + vec_a.set_w(0.0f); + vec_b.set_z(1.0f); + vec_b.set_w(1.0f); + } else if (max_type == NodeValue::k_vec3) { + vec_a.set_w(0.0f); + vec_b.set_w(1.0f); + } + } + + push_vector(output, std::max(val_a.type(), val_b.type()), + perform_add_sub_mult_div(operation, vec_a, + vec_b)); + break; + } + + case k_pair_matrix_vec: { + Matrix4x4 matrix = (val_a.type() == NodeValue::k_matrix) ? + val_a.to_matrix() : + val_b.to_matrix(); + Vector4D vec = (val_a.type() == NodeValue::k_matrix) ? + retrieve_vector(val_b) : + retrieve_vector(val_a); + + // Only valid operation is multiply + push_vector(output, std::max(val_a.type(), val_b.type()), + perform_mult(operation, vec, matrix)); + break; + } + + case k_pair_vec_number: { + Vector4D vec = (NodeValue::type_is_vector(val_a.type()) ? + retrieve_vector(val_a) : + retrieve_vector(val_b)); + float number = retrieve_number(NodeValue::type_is_vector(val_a.type()) ? + val_b : + val_a); + + // Only multiply and divide are valid operations + push_vector(output, + NodeValue::type_is_vector(val_a.type()) ? val_a.type() : + val_b.type(), + perform_mult_div(operation, vec, number)); + break; + } + + case k_pair_matrix_matrix: { + Matrix4x4 mat_a = val_a.to_matrix(); + Matrix4x4 mat_b = val_b.to_matrix(); + output->push(NodeValue::k_matrix, + perform_add_sub_mult(operation, mat_a, + mat_b), + this); + break; + } + + case k_pair_color_color: { + Color col_a = val_a.to_color(); + Color col_b = val_b.to_color(); + + // Only add and subtract are valid operations + output->push(NodeValue::k_color, + perform_add_sub(operation, col_a, col_b), + this); + break; + } + + case k_pair_number_color: { + Color col = (val_a.type() == NodeValue::k_color) ? val_a.to_color() : + val_b.to_color(); + float num = (val_a.type() == NodeValue::k_color) ? val_b.to_double() : + val_a.to_double(); + + // Only multiply and divide are valid operations + output->push(NodeValue::k_color, + perform_mult(operation, col, num), this); + break; + } + + case k_pair_sample_sample: { + SampleBuffer samples_a = val_a.to_samples(); + SampleBuffer samples_b = val_b.to_samples(); + + size_t max_samples = + std::max(samples_a.sample_count(), samples_b.sample_count()); + size_t min_samples = + std::min(samples_a.sample_count(), samples_b.sample_count()); + + SampleBuffer mixed_samples = + SampleBuffer(samples_a.audio_params(), max_samples); + + for (int i = 0; i < mixed_samples.audio_params().channel_count(); i++) { + // Mix samples that are in both buffers + for (size_t j = 0; j < min_samples; j++) { + mixed_samples.data(i)[j] = perform_all( + operation, samples_a.data(i)[j], samples_b.data(i)[j]); + } + } + + if (max_samples > min_samples) { + // Fill in remainder space with 0s + size_t remainder = max_samples - min_samples; + + const SampleBuffer &larger_buffer = + (max_samples == samples_a.sample_count()) ? samples_a : + samples_b; + + for (int i = 0; i < mixed_samples.audio_params().channel_count(); + i++) { + memcpy(&mixed_samples.data(i)[min_samples], + &larger_buffer.data(i)[min_samples], + remainder * sizeof(float)); + } + } + + output->push(NodeValue::k_samples, mixed_samples, this); + break; + } + + case k_pair_texture_color: + case k_pair_texture_number: + case k_pair_texture_texture: + case k_pair_texture_matrix: { + ShaderJob job; + job.set_shader_id(std::to_string(operation) + "." + + std::to_string(pairing) + "." + + std::to_string(val_a.type()) + "." + + std::to_string(val_b.type())); + + job.insert(param_a_in, val_a); + job.insert(param_b_in, val_b); + + bool operation_is_noop = false; + + const NodeValue &number_val = + val_a.type() == NodeValue::k_texture ? val_b : val_a; + const NodeValue &texture_val = + val_a.type() == NodeValue::k_texture ? val_a : val_b; + TexturePtr texture = texture_val.to_texture(); + + if (!texture) { + operation_is_noop = true; + } else if (pairing == k_pair_texture_number) { + if (number_is_no_op(operation, retrieve_number(number_val))) { + operation_is_noop = true; + } + } else if (pairing == k_pair_texture_matrix) { + // Only allow matrix multiplication + const Vector2D &sequence_res = globals.nonsquare_resolution(); + Vector2D texture_res(texture->params().width() * + texture->pixel_aspect_ratio().to_double(), + texture->params().height()); + + Matrix4x4 adjusted_matrix = + TransformDistortNode::adjust_matrix_by_resolutions( + number_val.to_matrix(), sequence_res, + Vector2D(texture->params().x(), texture->params().y()), texture_res); + + if (operation != k_op_multiply || adjusted_matrix.is_identity()) { + operation_is_noop = true; + } else { + // Replace with adjusted matrix + job.insert(val_a.type() == NodeValue::k_texture ? param_b_in : + param_a_in, + NodeValue(NodeValue::k_matrix, adjusted_matrix, + this)); + } + } + + if (operation_is_noop) { + // Just push texture as-is + output->push(texture_val); + } else { + // Push shader job + output->push(NodeValue::k_texture, + Texture::job(globals.vparams(), job), this); + } + break; + } + + case k_pair_sample_number: { + // Queue a sample job + const NodeValue &number_val = + val_a.type() == NodeValue::k_samples ? val_b : val_a; + const std::string &number_param = + val_a.type() == NodeValue::k_samples ? param_b_in : param_a_in; + + float number = retrieve_number(number_val); + + SampleBuffer buffer = val_a.type() == NodeValue::k_samples ? + val_a.to_samples() : + val_b.to_samples(); + + if (buffer.is_allocated()) { + if (is_input_static(number_param)) { + if (!number_is_no_op(operation, number)) { + for (int i = 0; i < buffer.audio_params().channel_count(); + i++) { +#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM) + // Use SSE instructions for optimization + perform_all_on_float_buffer_sse(operation, buffer.data(i), + number, 0, + buffer.sample_count()); +#else + PerformAllOnFloatBuffer(operation, buffer.data(i), + number, 0, + buffer.sample_count()); +#endif + } + } + + output->push(NodeValue::k_samples, buffer, this); + } else { + SampleJob job(globals.time(), + val_a.type() == NodeValue::k_samples ? val_a : + val_b); + job.insert(number_param, + NodeValue(NodeValue::k_float, number, this)); + output->push(NodeValue::k_samples, job, this); + } + } + break; + } + + case k_pair_none: + case k_pair_count: + break; + } +} + +void MathNodeBase::process_samples_internal(const NodeValueRow &values, + MathNodeBase::Operation operation, + const std::string ¶m_a_in, + const std::string ¶m_b_in, + const olive::SampleBuffer &input, + olive::SampleBuffer &output, + int index) const +{ + // This function is only used for sample+number pairing + NodeValue number_val = values.at(param_a_in); + + if (number_val.type() == NodeValue::k_none) { + number_val = values.at(param_b_in); + + if (number_val.type() == NodeValue::k_none) { + return; + } + } + + float number_flt = retrieve_number(number_val); + + for (int i = 0; i < output.audio_params().channel_count(); i++) { + output.data(i)[index] = perform_all( + operation, input.data(i)[index], number_flt); + } +} + +float MathNodeBase::retrieve_number(const NodeValue &val) +{ + if (val.type() == NodeValue::k_rational) { + return val.to_rational().to_double(); + } else { + return val.to_double(); + } +} + +bool MathNodeBase::number_is_no_op(const MathNodeBase::Operation &op, + const float &number) +{ + switch (op) { + case k_op_add: + case k_op_subtract: + if (number == 0.0f) { + return true; + } + break; + case k_op_multiply: + case k_op_divide: + case k_op_power: + // Same semantics as qFuzzyCompare(number, 1.0f) + if (std::abs(number - 1.0f) * 100000.0f <= + std::min(std::abs(number), 1.0f)) { + return true; + } + break; + } + + return false; +} + +MathNodeBase::PairingCalculator::PairingCalculator( + const NodeValueTable &table_a, const NodeValueTable &table_b) +{ + std::vector pair_likelihood_a = get_pair_likelihood(table_a); + std::vector pair_likelihood_b = get_pair_likelihood(table_b); + + int weight_a = std::max(0, table_b.count() - table_a.count()); + int weight_b = std::max(0, table_a.count() - table_b.count()); + + std::vector likelihoods(k_pair_count); + + for (int i = 0; i < k_pair_count; i++) { + if (pair_likelihood_a.at(i) == -1 || pair_likelihood_b.at(i) == -1) { + likelihoods[i] = -1; + } else { + likelihoods[i] = pair_likelihood_a.at(i) + weight_a + + pair_likelihood_b.at(i) + weight_b; + } + } + + most_likely_pairing_ = k_pair_none; + + for (int i = 0; i < int(likelihoods.size()); i++) { + if (likelihoods.at(i) > -1) { + if (most_likely_pairing_ == k_pair_none || + likelihoods.at(i) > likelihoods.at(most_likely_pairing_)) { + most_likely_pairing_ = static_cast(i); + } + } + } + + if (most_likely_pairing_ != k_pair_none) { + most_likely_value_a_ = + table_a.at(pair_likelihood_a.at(most_likely_pairing_)); + most_likely_value_b_ = + table_b.at(pair_likelihood_b.at(most_likely_pairing_)); + } +} + +std::vector +MathNodeBase::PairingCalculator::get_pair_likelihood(const NodeValueTable &table) +{ + std::vector likelihood(k_pair_count, -1); + + for (int i = 0; i < table.count(); i++) { + NodeValue::Type type = table.at(i).type(); + + int weight = i; + + if (NodeValue::type_is_vector(type)) { + likelihood[k_pair_vec_vec] = weight; + likelihood[k_pair_vec_number] = weight; + likelihood[k_pair_matrix_vec] = weight; + } else if (type == NodeValue::k_matrix) { + likelihood[k_pair_matrix_matrix] = weight; + likelihood[k_pair_matrix_vec] = weight; + likelihood[k_pair_texture_matrix] = weight; + } else if (type == NodeValue::k_color) { + likelihood[k_pair_color_color] = weight; + likelihood[k_pair_number_color] = weight; + likelihood[k_pair_texture_color] = weight; + } else if (NodeValue::type_is_numeric(type)) { + likelihood[k_pair_number_number] = weight; + likelihood[k_pair_vec_number] = weight; + likelihood[k_pair_number_color] = weight; + likelihood[k_pair_texture_number] = weight; + likelihood[k_pair_sample_number] = weight; + } else if (type == NodeValue::k_samples) { + likelihood[k_pair_sample_sample] = weight; + likelihood[k_pair_sample_number] = weight; + } else if (type == NodeValue::k_texture) { + likelihood[k_pair_texture_texture] = weight; + likelihood[k_pair_texture_number] = weight; + likelihood[k_pair_texture_color] = weight; + likelihood[k_pair_texture_matrix] = weight; + } + } + + return likelihood; +} + +bool MathNodeBase::PairingCalculator::found_most_likely_pairing() const +{ + return (most_likely_pairing_ > k_pair_none && + most_likely_pairing_ < k_pair_count); +} + +MathNodeBase::Pairing +MathNodeBase::PairingCalculator::get_most_likely_pairing() const +{ + return most_likely_pairing_; +} + +const NodeValue &MathNodeBase::PairingCalculator::get_most_likely_value_a() const +{ + return most_likely_value_a_; +} + +const NodeValue &MathNodeBase::PairingCalculator::get_most_likely_value_b() const +{ + return most_likely_value_b_; +} + +template +T MathNodeBase::perform_all(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_add: + return a + b; + case k_op_subtract: + return a - b; + case k_op_multiply: + return a * b; + case k_op_divide: + return a / b; + case k_op_power: + return std::pow(a, b); + } + + return a; +} + +template +T MathNodeBase::perform_mult_div(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_multiply: + return a * b; + case k_op_divide: + return a / b; + case k_op_add: + case k_op_subtract: + case k_op_power: + break; + } + + return a; +} + +template +T MathNodeBase::perform_add_sub(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_add: + return a + b; + case k_op_subtract: + return a - b; + case k_op_multiply: + case k_op_divide: + case k_op_power: + break; + } + + return a; +} + +template +T MathNodeBase::perform_mult(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_multiply: + return a * b; + case k_op_add: + case k_op_subtract: + case k_op_divide: + case k_op_power: + break; + } + + return a; +} + +template +T MathNodeBase::perform_add_sub_mult(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_add: + return a + b; + case k_op_subtract: + return a - b; + case k_op_multiply: + return a * b; + case k_op_divide: + case k_op_power: + break; + } + + return a; +} + +template +T MathNodeBase::perform_add_sub_mult_div(Operation operation, T a, U b) +{ + switch (operation) { + case k_op_add: + return a + b; + case k_op_subtract: + return a - b; + case k_op_multiply: + return a * b; + case k_op_divide: + return a / b; + case k_op_power: + break; + } + + return a; +} + +} diff --git a/src/node/src/math/math/mathbase.h b/src/node/src/math/math/mathbase.h new file mode 100644 index 000000000..f081d0188 --- /dev/null +++ b/src/node/src/math/math/mathbase.h @@ -0,0 +1,142 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MATHNODEBASE_H +#define OAK_MATHNODEBASE_H + +#include "node.h" +#include "olive/core/util/cpuoptimize.h" + +namespace olive +{ + +class MathNodeBase : public Node { +public: + MathNodeBase() = default; + + enum Operation { k_op_add, k_op_subtract, k_op_multiply, k_op_divide, k_op_power }; + + static std::string get_operation_name(Operation o); + +protected: + enum Pairing { + k_pair_none = -1, + + k_pair_number_number, + k_pair_vec_vec, + k_pair_matrix_matrix, + k_pair_color_color, + k_pair_texture_texture, + + k_pair_vec_number, + k_pair_matrix_vec, + k_pair_number_color, + k_pair_texture_number, + k_pair_texture_color, + k_pair_texture_matrix, + k_pair_sample_sample, + k_pair_sample_number, + + k_pair_count + }; + + class PairingCalculator { + public: + PairingCalculator(const NodeValueTable &table_a, + const NodeValueTable &table_b); + + bool found_most_likely_pairing() const; + Pairing get_most_likely_pairing() const; + + const NodeValue &get_most_likely_value_a() const; + const NodeValue &get_most_likely_value_b() const; + + private: + static std::vector get_pair_likelihood(const NodeValueTable &table); + + Pairing most_likely_pairing_; + + NodeValue most_likely_value_a_; + + NodeValue most_likely_value_b_; + }; + + template + static T perform_all(Operation operation, T a, U b); + + template + static T perform_mult_div(Operation operation, T a, U b); + + template + static T perform_add_sub(Operation operation, T a, U b); + + template + static T perform_mult(Operation operation, T a, U b); + + template + static T perform_add_sub_mult(Operation operation, T a, U b); + + template + static T perform_add_sub_mult_div(Operation operation, T a, U b); + + static void perform_all_on_float_buffer(Operation operation, float *a, float b, + int start, int end); + +#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM) + static void perform_all_on_float_buffer_sse(Operation operation, float *a, + float b, int start, int end); +#endif + + static std::string get_shader_uniform_type(const NodeValue::Type &type); + + static std::string get_shader_variable_call(const std::string &input_id, + const NodeValue::Type &type, + const std::string &coord_op = std::string()); + + static Vector4D retrieve_vector(const NodeValue &val); + + static float retrieve_number(const NodeValue &val); + + static bool number_is_no_op(const Operation &op, const float &number); + + ShaderCode get_shader_code_internal(const std::string &shader_id, + const std::string ¶m_a_in, + const std::string ¶m_b_in) const; + + void push_vector(NodeValueTable *output, NodeValue::Type type, + const Vector4D &vec) const; + + void value_internal(Operation operation, Pairing pairing, + const std::string ¶m_a_in, const NodeValue &val_a, + const std::string ¶m_b_in, const NodeValue &val_b, + const NodeGlobals &globals, + NodeValueTable *output) const; + + void process_samples_internal(const NodeValueRow &values, Operation operation, + const std::string ¶m_a_in, + const std::string ¶m_b_in, + const SampleBuffer &input, SampleBuffer &output, + int index) const; +}; + +} + +#endif // OAK_MATHNODEBASE_H diff --git a/src/node/src/math/merge/CMakeLists.txt b/src/node/src/math/merge/CMakeLists.txt new file mode 100644 index 000000000..90705b220 --- /dev/null +++ b/src/node/src/math/merge/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/math/merge/merge.h + node/math/merge/merge.cpp + PARENT_SCOPE +) diff --git a/src/node/src/math/merge/merge.cpp b/src/node/src/math/merge/merge.cpp new file mode 100644 index 000000000..7c7fd71ae --- /dev/null +++ b/src/node/src/math/merge/merge.cpp @@ -0,0 +1,104 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "merge.h" + +#include "filefunctions.h" +#include "traverser.h" + +namespace olive +{ + +const std::string MergeNode::k_base_in = "base_in"; +const std::string MergeNode::k_blend_in = "blend_in"; + +#define super Node + +MergeNode::MergeNode() +{ + add_input(k_base_in, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + add_input(k_blend_in, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + + set_flag(k_dont_show_in_param_view); +} + +std::string MergeNode::name() const +{ + return "Merge"; +} + +std::string MergeNode::id() const +{ + return "org.olivevideoeditor.Olive.merge"; +} + +std::vector MergeNode::category() const +{ + return { k_category_math }; +} + +std::string MergeNode::description() const +{ + return "Merge two textures together."; +} + +void MergeNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_base_in, "Base"); + + set_input_name(k_blend_in, "Blend"); +} + +ShaderCode MergeNode::get_shader_code(const ShaderRequest &request) const +{ + (void) request; + + return ShaderCode( + FileFunctions::read_file_as_string(":/shaders/alphaover.frag")); +} + +void MergeNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + TexturePtr base_tex = value.at(k_base_in).to_texture(); + TexturePtr blend_tex = value.at(k_blend_in).to_texture(); + + if (base_tex || blend_tex) { + if (!base_tex || (blend_tex && blend_tex->channel_count() < + VideoParams::k_rgba_channel_count)) { + // We only have a blend texture or the blend texture is RGB only, no need to alpha over + table->push(value.at(k_blend_in)); + } else if (!blend_tex) { + // We only have a base texture, no need to alpha over + table->push(value.at(k_base_in)); + } else { + table->push(NodeValue::k_texture, base_tex->to_job(ShaderJob(value)), + this); + } + } +} + +} diff --git a/src/node/src/math/merge/merge.h b/src/node/src/math/merge/merge.h new file mode 100644 index 000000000..9520283e8 --- /dev/null +++ b/src/node/src/math/merge/merge.h @@ -0,0 +1,59 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_MERGENODE_H +#define OAK_MERGENODE_H + +#include "node.h" + +namespace olive +{ + +class MergeNode : public Node { +public: + MergeNode(); + + NODE_DEFAULT_FUNCTIONS(MergeNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual ShaderCode + get_shader_code(const ShaderRequest &request) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_base_in; + static const std::string k_blend_in; + +private: + NodeInput *base_in_; + + NodeInput *blend_in_; +}; + +} + +#endif // OAK_MERGENODE_H diff --git a/src/node/src/math/trigonometry/CMakeLists.txt b/src/node/src/math/trigonometry/CMakeLists.txt new file mode 100644 index 000000000..0d1102046 --- /dev/null +++ b/src/node/src/math/trigonometry/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/math/trigonometry/trigonometry.h + node/math/trigonometry/trigonometry.cpp + PARENT_SCOPE +) diff --git a/src/node/src/math/trigonometry/trigonometry.cpp b/src/node/src/math/trigonometry/trigonometry.cpp new file mode 100644 index 000000000..1979f4060 --- /dev/null +++ b/src/node/src/math/trigonometry/trigonometry.cpp @@ -0,0 +1,120 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "trigonometry.h" + +namespace olive +{ + +const std::string TrigonometryNode::k_method_in = "method_in"; +const std::string TrigonometryNode::k_x_in = "x_in"; + +#define super Node + +TrigonometryNode::TrigonometryNode() +{ + add_input(k_method_in, NodeValue::k_combo, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_x_in, NodeValue::k_float, 0.0); +} + +std::string TrigonometryNode::name() const +{ + return "Trigonometry"; +} + +std::string TrigonometryNode::id() const +{ + return "org.olivevideoeditor.Olive.trigonometry"; +} + +std::vector TrigonometryNode::category() const +{ + return { k_category_math }; +} + +std::string TrigonometryNode::description() const +{ + return "Perform a trigonometry operation on a value."; +} + +void TrigonometryNode::retranslate() +{ + super::retranslate(); + + StringList strings = { "Sine", + "Cosine", + "Tangent", + "Inverse Sine", + "Inverse Cosine", + "Inverse Tangent", + "Hyperbolic Sine", + "Hyperbolic Cosine", + "Hyperbolic Tangent" }; + + set_combo_box_strings(k_method_in, strings); + + set_input_name(k_method_in, "Method"); + + set_input_name(k_x_in, "Value"); +} + +void TrigonometryNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + double x = value.at(k_x_in).to_double(); + + switch (static_cast(get_standard_value(k_method_in).to_int())) { + case k_op_sine: + x = std::sin(x); + break; + case k_op_cosine: + x = std::cos(x); + break; + case k_op_tangent: + x = std::tan(x); + break; + case k_op_arc_sine: + x = std::asin(x); + break; + case k_op_arc_cosine: + x = std::acos(x); + break; + case k_op_arc_tangent: + x = std::atan(x); + break; + case k_op_hyp_sine: + x = std::sinh(x); + break; + case k_op_hyp_cosine: + x = std::cosh(x); + break; + case k_op_hyp_tangent: + x = std::tanh(x); + break; + } + + table->push(NodeValue::k_float, x, this); +} + +} diff --git a/src/node/src/math/trigonometry/trigonometry.h b/src/node/src/math/trigonometry/trigonometry.h new file mode 100644 index 000000000..33c4872e0 --- /dev/null +++ b/src/node/src/math/trigonometry/trigonometry.h @@ -0,0 +1,65 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TRIGNODE_H +#define OAK_TRIGNODE_H + +#include "node.h" + +namespace olive +{ + +class TrigonometryNode : public Node { +public: + TrigonometryNode(); + + NODE_DEFAULT_FUNCTIONS(TrigonometryNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_method_in; + static const std::string k_x_in; + +private: + enum Operation { + k_op_sine, + k_op_cosine, + k_op_tangent, + k_op_arc_sine, + k_op_arc_cosine, + k_op_arc_tangent, + k_op_hyp_sine, + k_op_hyp_cosine, + k_op_hyp_tangent + }; +}; + +} + +#endif // OAK_TRIGNODE_H diff --git a/src/node/src/mathtypes.h b/src/node/src/mathtypes.h new file mode 100644 index 000000000..c7849eb2a --- /dev/null +++ b/src/node/src/mathtypes.h @@ -0,0 +1,975 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEMATHTYPES_H +#define OAK_NODEMATHTYPES_H + +#include +#include + +namespace olive +{ + +/** + * @brief De-Qt replacement for QPointF. + * + * Plain old data 2D point with the subset of the QPointF API used by the + * node module. + */ +class PointF { +public: + PointF() + : x_(0.0) + , y_(0.0) + { + } + + PointF(double x, double y) + : x_(x) + , y_(y) + { + } + + double x() const + { + return x_; + } + double y() const + { + return y_; + } + + void set_x(double x) + { + x_ = x; + } + void set_y(double y) + { + y_ = y; + } + + double &rx() + { + return x_; + } + double &ry() + { + return y_; + } + + bool is_null() const + { + return x_ == 0.0 && y_ == 0.0; + } + + double manhattan_length() const + { + return std::fabs(x_) + std::fabs(y_); + } + + PointF &operator+=(const PointF &p) + { + x_ += p.x_; + y_ += p.y_; + return *this; + } + + PointF &operator-=(const PointF &p) + { + x_ -= p.x_; + y_ -= p.y_; + return *this; + } + + PointF &operator*=(double factor) + { + x_ *= factor; + y_ *= factor; + return *this; + } + + PointF &operator/=(double divisor) + { + x_ /= divisor; + y_ /= divisor; + return *this; + } + + bool operator==(const PointF &rhs) const + { + return x_ == rhs.x_ && y_ == rhs.y_; + } + + bool operator!=(const PointF &rhs) const + { + return !(*this == rhs); + } + + friend PointF operator+(PointF a, const PointF &b) + { + a += b; + return a; + } + + friend PointF operator-(PointF a, const PointF &b) + { + a -= b; + return a; + } + + friend PointF operator*(PointF p, double factor) + { + p *= factor; + return p; + } + + friend PointF operator*(double factor, PointF p) + { + p *= factor; + return p; + } + + friend PointF operator/(PointF p, double divisor) + { + p /= divisor; + return p; + } + + friend PointF operator-(const PointF &p) + { + return PointF(-p.x_, -p.y_); + } + +private: + double x_; + double y_; +}; + +/** + * @brief De-Qt replacement for QVector2D. + */ +class Vector2D { +public: + Vector2D() + : x_(0.0f) + , y_(0.0f) + { + } + + Vector2D(float x, float y) + : x_(x) + , y_(y) + { + } + + float x() const + { + return x_; + } + float y() const + { + return y_; + } + + void set_x(float x) + { + x_ = x; + } + void set_y(float y) + { + y_ = y; + } + + bool is_null() const + { + return x_ == 0.0f && y_ == 0.0f; + } + + /** + * @brief QVector2D::toPointF() equivalent. + */ + PointF to_point_f() const + { + return PointF(x_, y_); + } + + float length() const + { + return std::sqrt(x_ * x_ + y_ * y_); + } + + Vector2D normalized() const + { + float len = length(); + if (len == 0.0f) { + return Vector2D(); + } + return Vector2D(x_ / len, y_ / len); + } + + static float dot_product(const Vector2D &a, const Vector2D &b) + { + return a.x_ * b.x_ + a.y_ * b.y_; + } + + Vector2D &operator+=(const Vector2D &v) + { + x_ += v.x_; + y_ += v.y_; + return *this; + } + + Vector2D &operator-=(const Vector2D &v) + { + x_ -= v.x_; + y_ -= v.y_; + return *this; + } + + Vector2D &operator*=(float factor) + { + x_ *= factor; + y_ *= factor; + return *this; + } + + Vector2D &operator/=(float divisor) + { + x_ /= divisor; + y_ /= divisor; + return *this; + } + + bool operator==(const Vector2D &rhs) const + { + return x_ == rhs.x_ && y_ == rhs.y_; + } + + bool operator!=(const Vector2D &rhs) const + { + return !(*this == rhs); + } + + friend Vector2D operator+(Vector2D a, const Vector2D &b) + { + a += b; + return a; + } + + friend Vector2D operator-(Vector2D a, const Vector2D &b) + { + a -= b; + return a; + } + + friend Vector2D operator*(Vector2D v, float factor) + { + v *= factor; + return v; + } + + friend Vector2D operator*(float factor, Vector2D v) + { + v *= factor; + return v; + } + + friend Vector2D operator/(Vector2D v, float divisor) + { + v /= divisor; + return v; + } + + friend Vector2D operator-(const Vector2D &v) + { + return Vector2D(-v.x_, -v.y_); + } + + /** + * @brief Component-wise multiplication (QVector2D * QVector2D). + */ + friend Vector2D operator*(const Vector2D &a, const Vector2D &b) + { + return Vector2D(a.x_ * b.x_, a.y_ * b.y_); + } + + /** + * @brief Component-wise division (QVector2D / QVector2D). + */ + friend Vector2D operator/(const Vector2D &a, const Vector2D &b) + { + return Vector2D(a.x_ / b.x_, a.y_ / b.y_); + } + +private: + float x_; + float y_; +}; + +/** + * @brief De-Qt replacement for QVector3D. + */ +class Vector3D { +public: + Vector3D() + : x_(0.0f) + , y_(0.0f) + , z_(0.0f) + { + } + + Vector3D(float x, float y, float z) + : x_(x) + , y_(y) + , z_(z) + { + } + + float x() const + { + return x_; + } + float y() const + { + return y_; + } + float z() const + { + return z_; + } + + void set_x(float x) + { + x_ = x; + } + void set_y(float y) + { + y_ = y; + } + void set_z(float z) + { + z_ = z; + } + + bool is_null() const + { + return x_ == 0.0f && y_ == 0.0f && z_ == 0.0f; + } + + float length() const + { + return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_); + } + + Vector3D normalized() const + { + float len = length(); + if (len == 0.0f) { + return Vector3D(); + } + return Vector3D(x_ / len, y_ / len, z_ / len); + } + + static float dot_product(const Vector3D &a, const Vector3D &b) + { + return a.x_ * b.x_ + a.y_ * b.y_ + a.z_ * b.z_; + } + + static Vector3D cross_product(const Vector3D &a, const Vector3D &b) + { + return Vector3D(a.y_ * b.z_ - a.z_ * b.y_, a.z_ * b.x_ - a.x_ * b.z_, + a.x_ * b.y_ - a.y_ * b.x_); + } + + Vector3D &operator+=(const Vector3D &v) + { + x_ += v.x_; + y_ += v.y_; + z_ += v.z_; + return *this; + } + + Vector3D &operator-=(const Vector3D &v) + { + x_ -= v.x_; + y_ -= v.y_; + z_ -= v.z_; + return *this; + } + + Vector3D &operator*=(float factor) + { + x_ *= factor; + y_ *= factor; + z_ *= factor; + return *this; + } + + Vector3D &operator/=(float divisor) + { + x_ /= divisor; + y_ /= divisor; + z_ /= divisor; + return *this; + } + + bool operator==(const Vector3D &rhs) const + { + return x_ == rhs.x_ && y_ == rhs.y_ && z_ == rhs.z_; + } + + bool operator!=(const Vector3D &rhs) const + { + return !(*this == rhs); + } + + friend Vector3D operator+(Vector3D a, const Vector3D &b) + { + a += b; + return a; + } + + friend Vector3D operator-(Vector3D a, const Vector3D &b) + { + a -= b; + return a; + } + + friend Vector3D operator*(Vector3D v, float factor) + { + v *= factor; + return v; + } + + friend Vector3D operator*(float factor, Vector3D v) + { + v *= factor; + return v; + } + + friend Vector3D operator/(Vector3D v, float divisor) + { + v /= divisor; + return v; + } + + friend Vector3D operator-(const Vector3D &v) + { + return Vector3D(-v.x_, -v.y_, -v.z_); + } + + /** + * @brief Component-wise multiplication (QVector3D * QVector3D). + */ + friend Vector3D operator*(const Vector3D &a, const Vector3D &b) + { + return Vector3D(a.x_ * b.x_, a.y_ * b.y_, a.z_ * b.z_); + } + + /** + * @brief Component-wise division (QVector3D / QVector3D). + */ + friend Vector3D operator/(const Vector3D &a, const Vector3D &b) + { + return Vector3D(a.x_ / b.x_, a.y_ / b.y_, a.z_ / b.z_); + } + +private: + float x_; + float y_; + float z_; +}; + +/** + * @brief De-Qt replacement for QVector4D. + */ +class Vector4D { +public: + Vector4D() + : x_(0.0f) + , y_(0.0f) + , z_(0.0f) + , w_(0.0f) + { + } + + Vector4D(float x, float y, float z, float w) + : x_(x) + , y_(y) + , z_(z) + , w_(w) + { + } + + float x() const + { + return x_; + } + float y() const + { + return y_; + } + float z() const + { + return z_; + } + float w() const + { + return w_; + } + + void set_x(float x) + { + x_ = x; + } + void set_y(float y) + { + y_ = y; + } + void set_z(float z) + { + z_ = z; + } + void set_w(float w) + { + w_ = w; + } + + bool is_null() const + { + return x_ == 0.0f && y_ == 0.0f && z_ == 0.0f && w_ == 0.0f; + } + + float length() const + { + return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_ + w_ * w_); + } + + static float dot_product(const Vector4D &a, const Vector4D &b) + { + return a.x_ * b.x_ + a.y_ * b.y_ + a.z_ * b.z_ + a.w_ * b.w_; + } + + Vector4D &operator+=(const Vector4D &v) + { + x_ += v.x_; + y_ += v.y_; + z_ += v.z_; + w_ += v.w_; + return *this; + } + + Vector4D &operator-=(const Vector4D &v) + { + x_ -= v.x_; + y_ -= v.y_; + z_ -= v.z_; + w_ -= v.w_; + return *this; + } + + Vector4D &operator*=(float factor) + { + x_ *= factor; + y_ *= factor; + z_ *= factor; + w_ *= factor; + return *this; + } + + Vector4D &operator/=(float divisor) + { + x_ /= divisor; + y_ /= divisor; + z_ /= divisor; + w_ /= divisor; + return *this; + } + + bool operator==(const Vector4D &rhs) const + { + return x_ == rhs.x_ && y_ == rhs.y_ && z_ == rhs.z_ && w_ == rhs.w_; + } + + bool operator!=(const Vector4D &rhs) const + { + return !(*this == rhs); + } + + friend Vector4D operator+(Vector4D a, const Vector4D &b) + { + a += b; + return a; + } + + friend Vector4D operator-(Vector4D a, const Vector4D &b) + { + a -= b; + return a; + } + + friend Vector4D operator*(Vector4D v, float factor) + { + v *= factor; + return v; + } + + friend Vector4D operator*(float factor, Vector4D v) + { + v *= factor; + return v; + } + + friend Vector4D operator/(Vector4D v, float divisor) + { + v /= divisor; + return v; + } + + friend Vector4D operator-(const Vector4D &v) + { + return Vector4D(-v.x_, -v.y_, -v.z_, -v.w_); + } + + /** + * @brief Component-wise multiplication (QVector4D * QVector4D). + */ + friend Vector4D operator*(const Vector4D &a, const Vector4D &b) + { + return Vector4D(a.x_ * b.x_, a.y_ * b.y_, a.z_ * b.z_, a.w_ * b.w_); + } + + /** + * @brief Component-wise division (QVector4D / QVector4D). + */ + friend Vector4D operator/(const Vector4D &a, const Vector4D &b) + { + return Vector4D(a.x_ / b.x_, a.y_ / b.y_, a.z_ / b.z_, a.w_ / b.w_); + } + +private: + float x_; + float y_; + float z_; + float w_; +}; + +/** + * @brief De-Qt replacement for QMatrix4x4. + * + * Row-major 4x4 float matrix, matching QMatrix4x4's m[row][column] layout. + * Also covers the subset of QTransform used by the node module (2D affine + * operations are expressed on the Z plane, as QTransform does internally). + */ +class Matrix4x4 { +public: + /** + * @brief Construct an identity matrix (same as QMatrix4x4's default). + */ + Matrix4x4() + { + set_to_identity(); + } + + float operator()(int row, int column) const + { + return m_[row][column]; + } + + float &operator()(int row, int column) + { + return m_[row][column]; + } + + const float *const_data() const + { + return &m_[0][0]; + } + + bool is_identity() const + { + Matrix4x4 identity; + return *this == identity; + } + + void set_to_identity() + { + std::memset(m_, 0, sizeof(m_)); + for (int i = 0; i < 4; i++) { + m_[i][i] = 1.0f; + } + } + + Matrix4x4 transposed() const + { + Matrix4x4 r; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + r.m_[row][col] = m_[col][row]; + } + } + return r; + } + + /** + * @brief Return the inverse of this matrix. + * + * @param invertible If not null, set to whether the matrix is invertible. + * If not, an identity matrix is returned (QMatrix4x4 behavior). + */ + Matrix4x4 inverted(bool *invertible = nullptr) const; + + /** + * @brief QMatrix4x4::map(QPointF) equivalent. + * + * Maps p through the matrix treating it as (x, y, 0, 1) and dividing by + * the resulting w whenever w is not exactly 1 (Qt divides + * unconditionally in that case, including w == 0). + */ + PointF map(const PointF &p) const + { + double x = p.x() * m_[0][0] + p.y() * m_[0][1] + m_[0][3]; + double y = p.x() * m_[1][0] + p.y() * m_[1][1] + m_[1][3]; + double w = p.x() * m_[3][0] + p.y() * m_[3][1] + m_[3][3]; + if (w == 1.0) { + return PointF(x, y); + } + return PointF(x / w, y / w); + } + + /** + * @brief QTransform-style 2D translate (post-multiply on the Z plane). + */ + Matrix4x4 &translate(double x, double y) + { + return translate(x, y, 0.0); + } + + /** + * @brief QMatrix4x4-style translate. + */ + Matrix4x4 &translate(double x, double y, double z) + { + Matrix4x4 t; + t.m_[0][3] = float(x); + t.m_[1][3] = float(y); + t.m_[2][3] = float(z); + *this = *this * t; + return *this; + } + + /** + * @brief QTransform-style 2D scale. + */ + Matrix4x4 &scale(double x, double y) + { + return scale(x, y, 1.0); + } + + /** + * @brief QMatrix4x4-style scale. + */ + Matrix4x4 &scale(double x, double y, double z) + { + Matrix4x4 s; + s.m_[0][0] = float(x); + s.m_[1][1] = float(y); + s.m_[2][2] = float(z); + *this = *this * s; + return *this; + } + + /** + * @brief QTransform-style 2D rotation (degrees, around the Z axis). + */ + Matrix4x4 &rotate(double degrees) + { + double radians = degrees * M_PI / 180.0; + float c = float(std::cos(radians)); + float s = float(std::sin(radians)); + Matrix4x4 r; + r.m_[0][0] = c; + r.m_[0][1] = -s; + r.m_[1][0] = s; + r.m_[1][1] = c; + *this = *this * r; + return *this; + } + + Matrix4x4 &operator*=(const Matrix4x4 &rhs) + { + *this = *this * rhs; + return *this; + } + + Matrix4x4 &operator+=(const Matrix4x4 &rhs) + { + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + m_[row][col] += rhs.m_[row][col]; + } + } + return *this; + } + + Matrix4x4 &operator-=(const Matrix4x4 &rhs) + { + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + m_[row][col] -= rhs.m_[row][col]; + } + } + return *this; + } + + bool operator==(const Matrix4x4 &rhs) const + { + return std::memcmp(m_, rhs.m_, sizeof(m_)) == 0; + } + + bool operator!=(const Matrix4x4 &rhs) const + { + return !(*this == rhs); + } + + friend Matrix4x4 operator*(const Matrix4x4 &a, const Matrix4x4 &b) + { + Matrix4x4 r; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + float sum = 0.0f; + for (int k = 0; k < 4; k++) { + sum += a.m_[row][k] * b.m_[k][col]; + } + r.m_[row][col] = sum; + } + } + return r; + } + + friend Matrix4x4 operator+(Matrix4x4 a, const Matrix4x4 &b) + { + a += b; + return a; + } + + friend Matrix4x4 operator-(Matrix4x4 a, const Matrix4x4 &b) + { + a -= b; + return a; + } + + /** + * @brief Row-vector times matrix, mirroring QVector4D * QMatrix4x4. + */ + friend Vector4D operator*(const Vector4D &v, const Matrix4x4 &m) + { + return Vector4D(v.x() * m.m_[0][0] + v.y() * m.m_[1][0] + + v.z() * m.m_[2][0] + v.w() * m.m_[3][0], + v.x() * m.m_[0][1] + v.y() * m.m_[1][1] + + v.z() * m.m_[2][1] + v.w() * m.m_[3][1], + v.x() * m.m_[0][2] + v.y() * m.m_[1][2] + + v.z() * m.m_[2][2] + v.w() * m.m_[3][2], + v.x() * m.m_[0][3] + v.y() * m.m_[1][3] + + v.z() * m.m_[2][3] + v.w() * m.m_[3][3]); + } + + /** + * @brief Matrix times column-vector, mirroring QMatrix4x4 * QVector4D. + */ + friend Vector4D operator*(const Matrix4x4 &m, const Vector4D &v) + { + return Vector4D(m.m_[0][0] * v.x() + m.m_[0][1] * v.y() + + m.m_[0][2] * v.z() + m.m_[0][3] * v.w(), + m.m_[1][0] * v.x() + m.m_[1][1] * v.y() + + m.m_[1][2] * v.z() + m.m_[1][3] * v.w(), + m.m_[2][0] * v.x() + m.m_[2][1] * v.y() + + m.m_[2][2] * v.z() + m.m_[2][3] * v.w(), + m.m_[3][0] * v.x() + m.m_[3][1] * v.y() + + m.m_[3][2] * v.z() + m.m_[3][3] * v.w()); + } + +private: + float m_[4][4]; +}; + +inline Matrix4x4 Matrix4x4::inverted(bool *invertible) const +{ + // Gaussian elimination with partial pivoting over an augmented matrix + Matrix4x4 inv; + float aug[4][8]; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + aug[row][col] = m_[row][col]; + aug[row][col + 4] = inv.m_[row][col]; + } + } + + for (int col = 0; col < 4; col++) { + int pivot = col; + for (int row = col + 1; row < 4; row++) { + if (std::fabs(aug[row][col]) > std::fabs(aug[pivot][col])) { + pivot = row; + } + } + + if (aug[pivot][col] == 0.0f) { + if (invertible) { + *invertible = false; + } + return Matrix4x4(); + } + + if (pivot != col) { + for (int k = 0; k < 8; k++) { + std::swap(aug[col][k], aug[pivot][k]); + } + } + + float d = aug[col][col]; + for (int k = 0; k < 8; k++) { + aug[col][k] /= d; + } + + for (int row = 0; row < 4; row++) { + if (row != col) { + float f = aug[row][col]; + for (int k = 0; k < 8; k++) { + aug[row][k] -= f * aug[col][k]; + } + } + } + } + + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + inv.m_[row][col] = aug[row][col + 4]; + } + } + + if (invertible) { + *invertible = true; + } + return inv; +} + +} + +#endif // OAK_NODEMATHTYPES_H diff --git a/src/node/src/node.cpp b/src/node/src/node.cpp new file mode 100644 index 000000000..326117cb9 --- /dev/null +++ b/src/node/src/node.cpp @@ -0,0 +1,2731 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node.h" + +#ifndef _WIN32 +#include +#endif + +#include +#include +#include +#include + +#include "lerp.h" +#include "config/config.h" +#include "group/group.h" +#include "project/serializer/typeserializer.h" +#include "nodeundo.h" +#include "project.h" +#include "serializeddata.h" +#include "ui/colorcoding.h" + +namespace olive +{ + +/** + * @brief QString::number() equivalent for doubles ('g', 6 significant digits) + */ +static std::string number_to_string(double d) +{ + char buf[64]; + snprintf(buf, sizeof(buf), "%g", d); + return buf; +} + +const std::string Node::k_enabled_input = "enabled_in"; + +Node::Node() + : override_color_(-1) + , folder_(nullptr) + , parent_(nullptr) + , flags_(k_none) + , caches_enabled_(true) +{ + add_input(k_enabled_input, NodeValue::k_boolean, true); + + video_cache_ = new FrameHashCache(this); + thumbnail_cache_ = new ThumbnailCache(this); + audio_cache_ = new AudioPlaybackCache(this); + waveform_cache_ = new AudioWaveformCache(this); + + waveform_cache_->set_saving_enabled(false); +} + +Node::~Node() +{ + // Disconnect all edges + disconnect_all(); + + // Remove self from the graph while we're still a fully formed Node + set_parent(nullptr); + + // Remove all immediates + for (const auto &pair : standard_immediates_) { + delete pair.second; + } + for (auto it = array_immediates_.cbegin(); it != array_immediates_.cend(); + it++) { + for (NodeInputImmediate *i : it->second) { + delete i; + } + } +} + +std::string Node::short_name() const +{ + return name(); +} + +std::string Node::description() const +{ + // Return an empty string by default + return std::string(); +} + +void Node::retranslate() +{ + set_input_name(k_enabled_input, "Enabled"); +} + +Variant Node::data(const DataType &d) const +{ + if (d == icon) { + // Just a meaningless default icon to be used where necessary + return std::string("new"); + } + + return Variant(); +} + +bool Node::set_node_position_in_context(Node *node, const PointF &pos) +{ + auto it = context_positions_.find(node); + Position p = it != context_positions_.end() ? it->second : Position(); + + p.position = pos; + + return set_node_position_in_context(node, p); +} + +bool Node::set_node_position_in_context(Node *node, const Position &pos) +{ + bool added = !context_contains_node(node); + context_positions_[node] = pos; + + return added; +} + +bool Node::remove_node_from_context(Node *node) +{ + if (context_contains_node(node)) { + context_positions_.erase(node); + return true; + } else { + return false; + } +} + +Color Node::color() const +{ + int c; + + if (override_color_ >= 0) { + c = override_color_; + } else { + c = OAK_CONFIG_STR("CatColor" + std::to_string(int(this->category().front()))) + .to_int(); + } + + return ColorCoding::get_color(c); +} + +void Node::connect_edge(Node *output, const NodeInput &input) +{ + // Ensure graph is the same + assert(input.node()->parent() == output->parent()); + + // Ensure a connection isn't getting overwritten + assert(input.node()->input_connections().find(input) == + input.node()->input_connections().end()); + + // Insert connection on both sides + input.node()->input_connections_[input] = output; + output->output_connections_.push_back( + std::pair({ output, input })); + + // Call internal events + input.node()->InputConnectedEvent(input.input(), input.element(), output); + output->OutputConnectedEvent(input); + + // Invalidate all if this node isn't ignoring this input + if (!(input.node()->get_input_flags(input.input()) & + k_input_flag_ignore_invalidations)) { + input.node()->invalidate_all(input.input(), input.element()); + } +} + +void Node::disconnect_edge(Node *output, const NodeInput &input, bool silent) +{ + // Ensure graph is the same + assert(input.node()->parent() == output->parent()); + + // Ensure connection exists + assert(input.node()->input_connections().at(input) == output); + + // Remove connection from both sides + InputConnections &inputs = input.node()->input_connections_; + inputs.erase(inputs.find(input)); + + OutputConnections &outputs = output->output_connections_; + outputs.erase(std::find(outputs.begin(), outputs.end(), + std::pair({ output, input }))); + + if (silent) { + // Teardown-only mode: just drop the edge from both maps. No events, + // no signals, no invalidation — the whole graph is being destroyed + // and handlers would touch half-destroyed nodes. + return; + } + + if (std::getenv("OAK_DEBUG_EDGES")) { + fprintf(stderr, "EDGE-DEBUG: disconnect_edge %p -> %p (%s)\n", + (void *)output, (void *)input.node(), input.input().c_str()); + } + + // Call internal events + input.node()->InputDisconnectedEvent(input.input(), input.element(), + output); + output->OutputDisconnectedEvent(input); + + if (!(input.node()->get_input_flags(input.input()) & + k_input_flag_ignore_invalidations)) { + input.node()->invalidate_all(input.input(), input.element()); + } +} + +void Node::copy_cache_uuids_from(Node *n) +{ + video_cache_->set_uuid(n->video_cache_->get_uuid()); + audio_cache_->set_uuid(n->audio_cache_->get_uuid()); + thumbnail_cache_->set_uuid(n->thumbnail_cache_->get_uuid()); + waveform_cache_->set_uuid(n->waveform_cache_->get_uuid()); +} + +std::string Node::get_input_name(const std::string &id) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + return i->human_name; + } else { + report_invalid_input("get name of", id, -1); + return std::string(); + } +} + +bool Node::is_input_hidden(const std::string &input) const +{ + return (get_input_flags(input) & k_input_flag_hidden); +} + +bool Node::is_input_connectable(const std::string &input) const +{ + return !(get_input_flags(input) & k_input_flag_not_connectable); +} + +bool Node::is_input_keyframable(const std::string &input) const +{ + return !(get_input_flags(input) & k_input_flag_not_keyframable); +} + +bool Node::is_input_keyframing(const std::string &input, int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->is_keyframing(); + } else { + report_invalid_input("get keyframing state of", input, element); + return false; + } +} + +void Node::set_input_is_keyframing(const std::string &input, bool e, int element) +{ + if (!is_input_keyframable(input)) { + fprintf(stderr, + "Ignored set keyframing of %s because this input is not keyframable\n", + input.c_str()); + return; + } + + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + imm->set_is_keyframing(e); + } else { + report_invalid_input("set keyframing state of", input, element); + } +} + +bool Node::is_input_connected(const std::string &input, int element) const +{ + return get_connected_output(input, element); +} + +Node *Node::get_connected_output(const std::string &input, int element) const +{ + for (auto it = input_connections_.cbegin(); it != input_connections_.cend(); + it++) { + if (it->first.input() == input && it->first.element() == element) { + return it->second; + } + } + + return nullptr; +} + +bool Node::is_using_standard_value(const std::string &input, int track, + int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->is_using_standard_value(track); + } else { + report_invalid_input("determine whether using standard value in", input, + element); + return true; + } +} + +NodeValue::Type Node::get_input_data_type(const std::string &id) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + return i->type; + } else { + report_invalid_input("get data type of", id, -1); + return NodeValue::k_none; + } +} + +void Node::set_input_data_type(const std::string &id, const NodeValue::Type &type) +{ + Input *input_meta = get_internal_input_data(id); + + if (input_meta) { + input_meta->type = type; + + int array_sz = input_array_size(id); + for (int i = -1; i < array_sz; i++) { + get_immediate(id, i)->set_data_type(type); + } + } else { + report_invalid_input("set data type of", id, -1); + } +} + +bool Node::has_input_property(const std::string &id, const std::string &name) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + return i->properties.count(name); + } else { + report_invalid_input("get property of", id, -1); + return false; + } +} + +std::map Node::get_input_properties(const std::string &id) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + return i->properties; + } else { + report_invalid_input("get property table of", id, -1); + return std::map(); + } +} + +Variant Node::get_input_property(const std::string &id, const std::string &name) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + auto it = i->properties.find(name); + return it != i->properties.end() ? it->second : Variant(); + } else { + report_invalid_input("get property of", id, -1); + return Variant(); + } +} + +void Node::set_input_property(const std::string &id, const std::string &name, + const Variant &value) +{ + Input *i = get_internal_input_data(id); + + if (i) { + i->properties[name] = value; + } else { + report_invalid_input("set property of", id, -1); + } +} + +SplitValue Node::get_split_value_at_time(const std::string &input, const Rational &time, + int element) const +{ + SplitValue vals; + + int nb_tracks = get_number_of_keyframe_tracks(input); + + for (int i = 0; i < nb_tracks; i++) { + vals.push_back(get_split_value_at_time_on_track(input, time, i, element)); + } + + return vals; +} + +Variant Node::get_split_value_at_time_on_track(const std::string &input, + const Rational &time, int track, + int element) const +{ + if (!is_using_standard_value(input, track, element)) { + const NodeKeyframeTrack &key_track = + get_keyframe_tracks(input, element).at(track); + + if (key_track.front()->time() >= time) { + // This time precedes any keyframe, so we just return the first value + return key_track.front()->value(); + } + + if (key_track.back()->time() <= time) { + // This time is after any keyframes so we return the last value + return key_track.back()->value(); + } + + NodeValue::Type type = get_input_data_type(input); + + // If we're here, the time must be somewhere in between the keyframes + NodeKeyframe *before = nullptr, *after = nullptr; + + int low = 0; + int high = int(key_track.size()) - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + NodeKeyframe *mid_key = key_track.at(mid); + NodeKeyframe *next_key = key_track.at(mid + 1); + + if (mid_key->time() <= time && next_key->time() > time) { + before = mid_key; + after = next_key; + break; + } else if (mid_key->time() < time) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + if (before) { + if (before->time() == time || + ((!NodeValue::type_can_be_interpolated(type) || + before->type() == NodeKeyframe::k_hold) && + after->time() > time)) { + // Time == keyframe time, so value is precise + return before->value(); + + } else if (after->time() == time) { + // Time == keyframe time, so value is precise + return after->value(); + + } else if (before->time() < time && after->time() > time) { + // We must interpolate between these keyframes + + double before_val, after_val, interpolated; + if (type == NodeValue::k_rational) { + // Keys for Rational inputs usually hold rationals, but may + // hold plain doubles, in which case we convert to Rational + // first to preserve the value + before_val = (before->value().can_convert() ? + before->value().value() : + Rational::from_double( + before->value().to_double())) + .to_double(); + after_val = (after->value().can_convert() ? + after->value().value() : + Rational::from_double( + after->value().to_double())) + .to_double(); + } else { + before_val = before->value().to_double(); + after_val = after->value().to_double(); + } + + if (before->type() == NodeKeyframe::k_bezier && + after->type() == NodeKeyframe::k_bezier) { + // Perform a cubic bezier with two control points + interpolated = Bezier::cubic_xto_y( + time.to_double(), + Imath::V2d(before->time().to_double(), before_val), + Imath::V2d(before->time().to_double() + + before->valid_bezier_control_out().x(), + before_val + + before->valid_bezier_control_out().y()), + Imath::V2d(after->time().to_double() + + after->valid_bezier_control_in().x(), + after_val + + after->valid_bezier_control_in().y()), + Imath::V2d(after->time().to_double(), after_val)); + + } else if (before->type() == NodeKeyframe::k_bezier || + after->type() == NodeKeyframe::k_bezier) { + // Perform a quadratic bezier with only one control point + + Imath::V2d control_point; + + if (before->type() == NodeKeyframe::k_bezier) { + control_point.x = + (before->valid_bezier_control_out().x() + + before->time().to_double()); + control_point.y = + (before->valid_bezier_control_out().y() + + before_val); + } else { + control_point.x = + (after->valid_bezier_control_in().x() + + after->time().to_double()); + control_point.y = + (after->valid_bezier_control_in().y() + after_val); + } + + // Interpolate value using quadratic beziers + interpolated = Bezier::quadratic_xto_y( + time.to_double(), + Imath::V2d(before->time().to_double(), before_val), + control_point, + Imath::V2d(after->time().to_double(), after_val)); + + } else { + // To have arrived here, the keyframes must both be linear + double period_progress = + (time.to_double() - before->time().to_double()) / + (after->time().to_double() - before->time().to_double()); + + interpolated = lerp(before_val, after_val, period_progress); + } + + if (type == NodeValue::k_rational) { + return Variant::from_value( + Rational::from_double(interpolated)); + } else { + return interpolated; + } + } + } else { + fprintf(stderr, "Binary search for keyframes failed\n"); + } + } + + return get_split_standard_value_on_track(input, track, element); +} + +Variant Node::get_default_value(const std::string &input) const +{ + NodeValue::Type type = get_input_data_type(input); + + return NodeValue::combine_track_values_into_normal_value( + type, get_split_default_value(input)); +} + +SplitValue Node::get_split_default_value(const std::string &input) const +{ + const Input *i = get_internal_input_data(input); + + if (i) { + return i->default_value; + } else { + report_invalid_input("retrieve default value of", input, -1); + return SplitValue(); + } +} + +Variant Node::get_split_default_value_on_track(const std::string &input, + int track) const +{ + SplitValue val = get_split_default_value(input); + if (track < int(val.size())) { + return val.at(track); + } else { + return Variant(); + } +} + +void Node::set_default_value(const std::string &input, const Variant &val) +{ + NodeValue::Type type = get_input_data_type(input); + + set_split_default_value( + input, NodeValue::split_normal_value_into_track_values(type, val)); +} + +void Node::set_split_default_value(const std::string &input, const SplitValue &val) +{ + Input *i = get_internal_input_data(input); + + if (i) { + i->default_value = val; + } else { + report_invalid_input("set default value of", input, -1); + } +} + +void Node::set_split_default_value_on_track(const std::string &input, + const Variant &val, int track) +{ + Input *i = get_internal_input_data(input); + + if (i) { + if (track < int(i->default_value.size())) { + i->default_value[track] = val; + } + } else { + report_invalid_input("set default value on track of", input, -1); + } +} + +const std::vector &Node::get_keyframe_tracks(const std::string &input, + int element) const +{ + return get_immediate(input, element)->keyframe_tracks(); +} + +std::vector Node::get_keyframes_at_time(const std::string &input, + const Rational &time, + int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->get_keyframe_at_time(time); + } else { + report_invalid_input("get keyframes at time from", input, element); + return std::vector(); + } +} + +NodeKeyframe *Node::get_keyframe_at_time_on_track(const std::string &input, + const Rational &time, int track, + int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->get_keyframe_at_time_on_track(time, track); + } else { + report_invalid_input("get keyframe at time on track from", input, + element); + return nullptr; + } +} + +NodeKeyframe::Type Node::get_best_keyframe_type_for_time_on_track(const std::string &input, + const Rational &time, + int track, + int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->get_best_keyframe_type_for_time(time, track); + } else { + report_invalid_input("get closest keyframe before a time from", input, + element); + return NodeKeyframe::k_default_type; + } +} + +int Node::get_number_of_keyframe_tracks(const std::string &id) const +{ + return NodeValue::get_number_of_keyframe_tracks(get_input_data_type(id)); +} + +NodeKeyframe *Node::get_earliest_keyframe(const std::string &id, int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->get_earliest_keyframe(); + } else { + report_invalid_input("get earliest keyframe from", id, element); + return nullptr; + } +} + +NodeKeyframe *Node::get_latest_keyframe(const std::string &id, int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->get_latest_keyframe(); + } else { + report_invalid_input("get latest keyframe from", id, element); + return nullptr; + } +} + +NodeKeyframe *Node::get_closest_keyframe_before_time(const std::string &id, + const Rational &time, + int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->get_closest_keyframe_before_time(time); + } else { + report_invalid_input("get closest keyframe before a time from", id, + element); + return nullptr; + } +} + +NodeKeyframe *Node::get_closest_keyframe_after_time(const std::string &id, + const Rational &time, + int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->get_closest_keyframe_after_time(time); + } else { + report_invalid_input("get closest keyframe after a time from", id, + element); + return nullptr; + } +} + +bool Node::has_keyframe_at_time(const std::string &id, const Rational &time, + int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->has_keyframe_at_time(time); + } else { + report_invalid_input("determine if it has a keyframe at a time from", id, + element); + return false; + } +} + +StringList Node::get_combo_box_strings(const std::string &id) const +{ + return get_input_property(id, "combo_str").to_string_list(); +} + +Variant Node::get_standard_value(const std::string &id, int element) const +{ + NodeValue::Type type = get_input_data_type(id); + + return NodeValue::combine_track_values_into_normal_value( + type, get_split_standard_value(id, element)); +} + +SplitValue Node::get_split_standard_value(const std::string &id, int element) const +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + return imm->get_split_standard_value(); + } else { + report_invalid_input("get standard value of", id, element); + return SplitValue(); + } +} + +Variant Node::get_split_standard_value_on_track(const std::string &input, int track, + int element) const +{ + NodeInputImmediate *imm = get_immediate(input, element); + + if (imm) { + return imm->get_split_standard_value_on_track(track); + } else { + report_invalid_input("get standard value of", input, element); + return Variant(); + } +} + +void Node::set_standard_value(const std::string &id, const Variant &value, + int element) +{ + NodeValue::Type type = get_input_data_type(id); + + set_split_standard_value( + id, NodeValue::split_normal_value_into_track_values(type, value), + element); +} + +void Node::set_split_standard_value(const std::string &id, const SplitValue &value, + int element) +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + imm->set_split_standard_value(value); + + for (int i = 0; i < int(value.size()); i++) { + if (is_using_standard_value(id, i, element)) { + // If this standard value is being used, we need to send a value changed signal + parameter_value_changed(id, element, + TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + break; + } + } + } else { + report_invalid_input("set standard value of", id, element); + } +} + +void Node::set_split_standard_value_on_track(const std::string &id, int track, + const Variant &value, int element) +{ + NodeInputImmediate *imm = get_immediate(id, element); + + if (imm) { + imm->set_standard_value_on_track(value, track); + + if (is_using_standard_value(id, track, element)) { + // If this standard value is being used, we need to send a value changed signal + parameter_value_changed(id, element, + TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } + } else { + report_invalid_input("set standard value of", id, element); + } +} + +bool Node::input_is_array(const std::string &id) const +{ + return get_input_flags(id) & k_input_flag_array; +} + +void Node::input_array_insert(const std::string &id, int index) +{ + // Add new input + array_resize_internal(id, input_array_size(id) + 1); + + // Move connections down + InputConnections copied_edges = input_connections(); + for (auto it = copied_edges.crbegin(); it != copied_edges.crend(); it++) { + if (it->first.input() == id && it->first.element() >= index) { + // Disconnect this and reconnect it one element down + NodeInput new_edge = it->first; + new_edge.set_element(new_edge.element() + 1); + + disconnect_edge(it->second, it->first); + connect_edge(it->second, new_edge); + } + } + + // Shift values and keyframes up one element + for (int i = input_array_size(id) - 1; i > index; i--) { + copy_values_of_element(this, this, id, i - 1, i); + } + + // Reset value of element we just "inserted" + clear_element(id, index); +} + +void Node::input_array_resize(const std::string &id, int size) +{ + if (input_array_size(id) == size) { + return; + } + + NodeArrayResizeCommand *c = new NodeArrayResizeCommand(this, id, size); + c->redo_now(); + delete c; +} + +void Node::input_array_remove(const std::string &id, int index) +{ + // Remove input + array_resize_internal(id, input_array_size(id) - 1); + + // Move connections up + InputConnections copied_edges = input_connections(); + for (auto it = copied_edges.cbegin(); it != copied_edges.cend(); it++) { + if (it->first.input() == id && it->first.element() >= index) { + // Disconnect this and reconnect it one element up if it's not the element being removed + disconnect_edge(it->second, it->first); + + if (it->first.element() > index) { + NodeInput new_edge = it->first; + new_edge.set_element(new_edge.element() - 1); + + connect_edge(it->second, new_edge); + } + } + } + + // Shift values and keyframes down one element + int arr_sz = input_array_size(id); + for (int i = index; i < arr_sz; i++) { + // Copying ArraySize()+1 is actually legal because immediates are never deleted + copy_values_of_element(this, this, id, i + 1, i); + } + + // Reset value of last element + clear_element(id, arr_sz); +} + +int Node::input_array_size(const std::string &id) const +{ + const Input *i = get_internal_input_data(id); + + if (i) { + return i->array_size; + } else { + report_invalid_input("retrieve array size of", id, -1); + return 0; + } +} + +void Node::set_value_hint_for_input(const std::string &input, const ValueHint &hint, + int element) +{ + value_hints_[{ input, element }] = hint; + + invalidate_all(input, element); +} + +const NodeKeyframeTrack &Node::get_track_from_keyframe(NodeKeyframe *key) const +{ + return get_immediate(key->input(), key->element()) + ->keyframe_tracks() + .at(key->track()); +} + +NodeInputImmediate *Node::get_immediate(const std::string &input, int element) const +{ + if (element == -1) { + auto it = standard_immediates_.find(input); + return it != standard_immediates_.end() ? it->second : nullptr; + } else { + auto it = array_immediates_.find(input); + if (it != array_immediates_.end()) { + const std::vector &imm_arr = it->second; + + if (element >= 0 && element < int(imm_arr.size())) { + return imm_arr.at(element); + } + } + } + + return nullptr; +} + +InputFlags Node::get_input_flags(const std::string &input) const +{ + const Input *i = get_internal_input_data(input); + + if (i) { + return i->flags; + } else { + report_invalid_input("retrieve flags of", input, -1); + return InputFlags(k_input_flag_normal); + } +} + +void Node::set_input_flag(const std::string &input, InputFlag f, bool on) +{ + Input *i = get_internal_input_data(input); + + if (i) { + if (on) { + i->flags |= f; + } else { + i->flags &= ~f; + } + } else { + report_invalid_input("set flags of", input, -1); + } +} + +void Node::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + // Do nothing + (void)value; + (void)globals; + (void)table; +} + +void Node::invalidate_cache(const TimeRange &range, const std::string &from, + int element, InvalidateCacheOptions options) +{ + (void)from; + (void)element; + + if (are_caches_enabled()) { + if (range.in() != range.out()) { + TimeRange vr = range.intersected(get_video_cache_range()); + if (vr.length() != 0) { + video_frame_cache()->invalidate(vr); + thumbnail_cache()->invalidate(vr); + } + TimeRange ar = range.intersected(get_audio_cache_range()); + if (ar.length() != 0) { + audio_playback_cache()->invalidate(ar); + waveform_cache()->invalidate(ar); + } + } + } + + send_invalidate_cache(range, options); +} + +TimeRange Node::input_time_adjustment(const std::string &, int, + const TimeRange &input_time, + bool clamp) const +{ + // Default behavior is no time adjustment at all + return input_time; +} + +TimeRange Node::output_time_adjustment(const std::string &, int, + const TimeRange &input_time) const +{ + // Default behavior is no time adjustment at all + return input_time; +} + +std::vector Node::copy_dependency_graph(const std::vector &nodes, + MultiUndoCommand *command) +{ + int nb_nodes = int(nodes.size()); + + std::vector copies(nb_nodes); + + for (int i = 0; i < nb_nodes; i++) { + // Create another of the same node + Node *c = nodes.at(i)->copy(); + + // Copy the values, but NOT the connections, since we'll be connecting to our own clones later + Node::copy_inputs(nodes.at(i), c, false); + + // Add to graph + Project *graph = nodes.at(i)->parent(); + if (command) { + command->add_child(new NodeAddCommand(graph, c)); + } else { + c->set_parent(graph); + } + + // Store in array at the same index as source + copies[i] = c; + } + + copy_dependency_graph(nodes, copies, command); + + return copies; +} + +void Node::copy_dependency_graph(const std::vector &src, + const std::vector &dst, + MultiUndoCommand *command) +{ + for (size_t i = 0; i < src.size(); i++) { + Node *src_node = src.at(i); + Node *dst_node = dst.at(i); + + for (auto it = src_node->input_connections_.cbegin(); + it != src_node->input_connections_.cend(); it++) { + // Determine if the connected node is in our src list + auto src_it = std::find(src.begin(), src.end(), it->second); + int connection_index = + src_it != src.end() ? int(src_it - src.begin()) : -1; + + if (connection_index > -1) { + // Find the equivalent node in the dst list + Node *copied_output = dst.at(connection_index); + NodeInput copied_input = + NodeInput(dst_node, it->first.input(), it->first.element()); + + if (command) { + command->add_child( + new NodeEdgeAddCommand(copied_output, copied_input)); + command->add_child(new NodeSetValueHintCommand( + copied_input, + src_node->get_value_hint_for_input( + copied_input.input(), copied_input.element()))); + } else { + connect_edge(copied_output, copied_input); + copied_input.node()->set_value_hint_for_input( + copied_input.input(), + src_node->get_value_hint_for_input(copied_input.input(), + copied_input.element()), + copied_input.element()); + } + } + } + } +} + +Node *Node::copy_node_and_dependency_graph_minus_items_internal( + std::map &created, Node *node, MultiUndoCommand *command) +{ + // Make a new node of the same type + Node *copy = node->copy(); + + // Add to map + created[node] = copy; + + // Add it to the same graph + command->add_child(new NodeAddCommand(node->parent(), copy)); + + // Copy context children + const PositionMap &map = node->get_context_positions(); + for (auto it = map.cbegin(); it != map.cend(); it++) { + // Add either the copy (if it exists) or the original node to the context + Node *child; + + if (it->first->is_item()) { + child = it->first; + } else { + auto created_it = created.find(it->first); + child = created_it != created.end() ? created_it->second : nullptr; + if (!child) { + child = copy_node_and_dependency_graph_minus_items_internal( + created, it->first, command); + } + } + + command->add_child(new NodeSetPositionCommand(child, copy, it->second)); + } + + // If this is a group, copy input and output passthroughs + if (NodeGroup *src_group = dynamic_cast(node)) { + NodeGroup *dst_group = static_cast(copy); + + for (auto it = src_group->get_input_passthroughs().cbegin(); + it != src_group->get_input_passthroughs().cend(); it++) { + // This node should have been created by the context loop above + NodeInput input = it->second; + auto created_it = created.find(input.node()); + input.set_node(created_it != created.end() ? created_it->second : + nullptr); + command->add_child( + new NodeGroupAddInputPassthrough(dst_group, input, it->first)); + } + + auto output_it = created.find(src_group->get_output_passthrough()); + command->add_child(new NodeGroupSetOutputPassthrough( + dst_group, output_it != created.end() ? output_it->second : nullptr)); + } + + // Copy values to the clone + copy_inputs(node, copy, false, command); + + // Go through input connections and copy if non-item and connect if item + for (auto it = node->input_connections_.cbegin(); + it != node->input_connections_.cend(); it++) { + NodeInput input = it->first; + Node *connected = it->second; + Node *connected_copy; + + if (connected->is_item()) { + // This is an item and we avoid copying those and just connect to them directly + connected_copy = connected; + } else { + // Non-item, we want to clone this too + auto created_it = created.find(connected); + connected_copy = + created_it != created.end() ? created_it->second : nullptr; + if (!connected_copy) { + connected_copy = copy_node_and_dependency_graph_minus_items_internal( + created, connected, command); + } + } + + NodeInput copied_input = input; + copied_input.set_node(copy); + command->add_child( + new NodeEdgeAddCommand(connected_copy, copied_input)); + command->add_child(new NodeSetValueHintCommand( + copied_input, + node->get_value_hint_for_input(input.input(), input.element()))); + } + + return copy; +} + +Node *Node::copy_node_and_dependency_graph_minus_items(Node *node, + MultiUndoCommand *command) +{ + std::map created; + + return copy_node_and_dependency_graph_minus_items_internal(created, node, command); +} + +Node *Node::copy_node_in_graph(Node *node, MultiUndoCommand *command) +{ + Node *copy; + + if (OAK_CONFIG("SplitClipsCopyNodes").to_bool()) { + copy = Node::copy_node_and_dependency_graph_minus_items(node, command); + } else { + copy = node->copy(); + + command->add_child(new NodeAddCommand(node->parent(), copy)); + + copy_inputs(node, copy, true, command); + + const PositionMap &map = node->get_context_positions(); + for (auto it = map.cbegin(); it != map.cend(); it++) { + // Add to the context + command->add_child( + new NodeSetPositionCommand(it->first, copy, it->second)); + } + } + + return copy; +} + +void Node::send_invalidate_cache(const TimeRange &range, + const InvalidateCacheOptions &options) +{ + // During project teardown, don't propagate invalidation across edges: + // the nodes on the other side may already be half-destroyed. + if (Project *p = project()) { + if (p->is_being_cleared()) { + return; + } + } + + for (const OutputConnection &conn : output_connections_) { + // Send clear cache signal to the Node + const NodeInput &in = conn.second; + + in.node()->invalidate_cache(range, in.input(), in.element(), options); + } +} + +void Node::invalidate_all(const std::string &input, int element) +{ + if (Project *p = project()) { + if (p->is_being_cleared()) { + return; + } + } + + invalidate_cache(TimeRange(RATIONAL_MIN, RATIONAL_MAX), input, element); +} + +bool Node::link(Node *a, Node *b) +{ + if (a == b || !a || !b) { + return false; + } + + if (are_linked(a, b)) { + return false; + } + + a->links_.push_back(b); + b->links_.push_back(a); + + a->LinkChangeEvent(); + b->LinkChangeEvent(); + + return true; +} + +bool Node::unlink(Node *a, Node *b) +{ + if (!are_linked(a, b)) { + return false; + } + + a->links_.erase(std::find(a->links_.begin(), a->links_.end(), b)); + b->links_.erase(std::find(b->links_.begin(), b->links_.end(), a)); + + a->LinkChangeEvent(); + b->LinkChangeEvent(); + + return true; +} + +bool Node::are_linked(Node *a, Node *b) +{ + return std::find(a->links_.begin(), a->links_.end(), b) != a->links_.end(); +} + +bool Node::load(XmlStreamReader *reader, SerializedData *data) +{ + unsigned int version = 0; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + uintptr_t ptr = strtoull(attr.value.c_str(), nullptr, 10); + data->node_ptrs[ptr] = this; + } else if (attr.name == "version") { + version = unsigned(strtoul(attr.value.c_str(), nullptr, 10)); + } + } + + (void)version; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + load_input(reader, data); + } else if (reader->name() == "label") { + this->set_label(reader->read_element_text()); + } else if (reader->name() == "color") { + this->set_override_color( + int(strtol(reader->read_element_text().c_str(), nullptr, 10))); + } else if (reader->name() == "links") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "link") { + data->block_links.push_back( + { this, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "custom") { + if (!load_custom(reader, data)) { + return false; + } + } else if (reader->name() == "connections") { + // Load connections + while (xml_read_next_start_element(reader)) { + if (reader->name() == "connection") { + std::string param_id; + int ele = -1; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "element") { + ele = int(strtol(attr.value.c_str(), nullptr, 10)); + } else if (attr.name == "input") { + param_id = attr.value; + } + } + + // Translate IDs renamed after older project files were + // written + param_id = get_input_id_for_legacy_id(param_id); + + std::string output_node_id; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "output") { + output_node_id = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + data->desired_connections.push_back( + { NodeInput(this, param_id, ele), + strtoull(output_node_id.c_str(), nullptr, 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "hints") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "hint") { + std::string input; + int element = -1; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "input") { + input = attr.value; + } else if (attr.name == "element") { + element = int(strtol(attr.value.c_str(), nullptr, 10)); + } + } + + Node::ValueHint vh; + if (!vh.load(reader)) { + return false; + } + this->set_value_hint_for_input(input, vh, element); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "context") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t node_ptr = 0; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + node_ptr = strtoull(attr.value.c_str(), nullptr, 10); + } + } + + if (node_ptr) { + Node::Position node_pos; + if (!node_pos.load(reader)) { + return false; + } + data->positions[this][node_ptr] = node_pos; + } else { + return false; + } + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "caches") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "audio") { + this->audio_playback_cache()->set_uuid(reader->read_element_text()); + } else if (reader->name() == "video") { + this->video_frame_cache()->set_uuid(reader->read_element_text()); + } else if (reader->name() == "thumb") { + this->thumbnail_cache()->set_uuid(reader->read_element_text()); + } else if (reader->name() == "waveform") { + this->waveform_cache()->set_uuid(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + this->LoadFinishedEvent(); + + return true; +} + +void Node::save(XmlStreamWriter *writer) const +{ + writer->write_attribute("version", std::to_string(1)); + writer->write_attribute("id", this->id()); + writer->write_attribute("ptr", + std::to_string(reinterpret_cast(this))); + + if (!this->get_label().empty()) { + writer->write_text_element("label", this->get_label()); + } + + if (this->get_override_color() != -1) { + writer->write_text_element("color", + std::to_string(this->get_override_color())); + } + + for (const std::string &input : this->inputs()) { + writer->write_start_element("input"); + + save_input(writer, input); + + writer->write_end_element(); // input + } + + if (!this->links().empty()) { + writer->write_start_element("links"); + for (Node *link : this->links()) { + writer->write_text_element( + "link", std::to_string(reinterpret_cast(link))); + } + writer->write_end_element(); // links + } + + if (!this->input_connections().empty()) { + writer->write_start_element("connections"); + for (auto it = this->input_connections().cbegin(); + it != this->input_connections().cend(); it++) { + writer->write_start_element("connection"); + + writer->write_attribute("input", it->first.input()); + writer->write_attribute("element", + std::to_string(it->first.element())); + + writer->write_text_element( + "output", + std::to_string(reinterpret_cast(it->second))); + + writer->write_end_element(); // connection + } + writer->write_end_element(); // connections + } + + if (!this->get_value_hints().empty()) { + writer->write_start_element("hints"); + for (auto it = this->get_value_hints().cbegin(); + it != this->get_value_hints().cend(); it++) { + writer->write_start_element("hint"); + + writer->write_attribute("input", it->first.input); + writer->write_attribute("element", + std::to_string(it->first.element)); + + it->second.save(writer); + + writer->write_end_element(); // hint + } + writer->write_end_element(); // hints + } + + const Node::PositionMap &map = this->get_context_positions(); + + if (!map.empty()) { + writer->write_start_element("context"); + + for (auto jt = map.cbegin(); jt != map.cend(); jt++) { + writer->write_start_element("node"); + writer->write_attribute( + "ptr", std::to_string(reinterpret_cast(jt->first))); + jt->second.save(writer); + writer->write_end_element(); // node + } + + writer->write_end_element(); // context + } + + writer->write_start_element("caches"); + + writer->write_text_element("audio", + this->audio_playback_cache()->get_uuid()); + writer->write_text_element("video", + this->video_frame_cache()->get_uuid()); + writer->write_text_element("thumb", + this->thumbnail_cache()->get_uuid()); + writer->write_text_element("waveform", + this->waveform_cache()->get_uuid()); + + writer->write_end_element(); // caches + + writer->write_start_element("custom"); + + save_custom(writer); + + writer->write_end_element(); // custom +} + +bool Node::load_custom(XmlStreamReader *reader, SerializedData *data) +{ + reader->skip_current_element(); + return true; +} + +void Node::PostLoadEvent(SerializedData *data) +{ + // Resolve positions + auto pos_it = data->positions.find(this); + if (pos_it == data->positions.end()) { + return; + } + + for (auto jt = pos_it->second.cbegin(); jt != pos_it->second.cend(); jt++) { + auto node_it = data->node_ptrs.find(jt->first); + if (node_it != data->node_ptrs.end() && node_it->second) { + this->set_node_position_in_context(node_it->second, jt->second); + } + } +} + +std::string Node::get_input_id_for_legacy_id(const std::string &id) const +{ + return id; +} + +bool Node::load_input(XmlStreamReader *reader, SerializedData *data) +{ + if (dynamic_cast(this)) { + // Ignore input of group + reader->skip_current_element(); + return true; + } + + std::string param_id; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + param_id = attr.value; + + break; + } + } + + if (param_id.empty()) { + fprintf(stderr, "Failed to load parameter with missing ID\n"); + reader->skip_current_element(); + return false; + } + + // Translate IDs renamed after older project files were written + param_id = get_input_id_for_legacy_id(param_id); + + if (!this->has_input_with_id(param_id)) { + fprintf(stderr, "Failed to load parameter that didn't exist: %s\n", + param_id.c_str()); + reader->skip_current_element(); + return false; + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + // Load primary immediate + if (!load_immediate(reader, param_id, -1, data)) { + return false; + } + } else if (reader->name() == "subelements") { + // Load subelements + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "count") { + this->input_array_resize( + param_id, int(strtol(attr.value.c_str(), nullptr, 10))); + } + } + + int element_counter = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + if (!load_immediate(reader, param_id, element_counter, + data)) { + return false; + } + + element_counter++; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void Node::save_input(XmlStreamWriter *writer, const std::string &id) const +{ + writer->write_attribute("id", id); + + writer->write_start_element("primary"); + + save_immediate(writer, id, -1); + + writer->write_end_element(); // primary + + int arr_sz = this->input_array_size(id); + + if (arr_sz > 0) { + writer->write_start_element("subelements"); + + writer->write_attribute("count", std::to_string(arr_sz)); + + for (int i = 0; i < arr_sz; i++) { + writer->write_start_element("element"); + + save_immediate(writer, id, i); + + writer->write_end_element(); // element + } + + writer->write_end_element(); // subelements + } +} + +bool Node::load_immediate(XmlStreamReader *reader, const std::string &input, + int element, SerializedData *data) +{ + NodeValue::Type data_type = this->get_input_data_type(input); + + // HACK: SubtitleParams contain the actual subtitle data, so loading/replacing it will overwrite + // the valid subtitles. We hack around it by simply skipping loading subtitles, we'll see + // if this ends up being an issue in the future. + if (data_type == NodeValue::k_subtitle_params) { + reader->skip_current_element(); + return true; + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + // Load standard value + int val_index = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + Variant value_on_track; + + if (data_type == NodeValue::k_video_params) { + VideoParams vp; + vp.load(reader); + value_on_track = Variant::from_value(vp); + } else if (data_type == NodeValue::k_audio_params) { + AudioParams ap = + TypeSerializer::load_audio_params(reader); + value_on_track = Variant::from_value(ap); + } else { + std::string value_text = reader->read_element_text(); + + if (!value_text.empty()) { + value_on_track = NodeValue::string_to_value( + data_type, value_text, true); + } + } + + this->set_split_standard_value_on_track(input, val_index, + value_on_track, element); + + val_index++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "keyframing") { + bool k = strtol(reader->read_element_text().c_str(), nullptr, 10); + if (this->is_input_keyframable(input)) { + this->set_input_is_keyframing(input, k, element); + } + } else if (reader->name() == "keyframes") { + int track = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "key") { + // Constructed with this node as parent (replaces + // the former setParent() call); load() overwrites + // the placeholder time/type/value below + NodeKeyframe *key = new NodeKeyframe( + Rational(), Variant(), NodeKeyframe::k_linear, + track, element, input, this); + + if (!key->load(reader, data_type)) { + delete key; + return false; + } + add_keyframe(key); + } else { + reader->skip_current_element(); + } + } + + track++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "csinput") { + this->set_input_property(input, "col_input", + reader->read_element_text()); + } else if (reader->name() == "csdisplay") { + this->set_input_property(input, "col_display", + reader->read_element_text()); + } else if (reader->name() == "csview") { + this->set_input_property(input, "col_view", + reader->read_element_text()); + } else if (reader->name() == "cslook") { + this->set_input_property(input, "col_look", + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void Node::save_immediate(XmlStreamWriter *writer, const std::string &input, + int element) const +{ + bool is_keyframing = this->is_input_keyframing(input, element); + + if (this->is_input_keyframable(input)) { + writer->write_text_element("keyframing", + std::to_string(int(is_keyframing))); + } + + NodeValue::Type data_type = this->get_input_data_type(input); + + // Write standard value + writer->write_start_element("standard"); + + for (const Variant &v : this->get_split_standard_value(input, element)) { + writer->write_start_element("track"); + + if (data_type == NodeValue::k_video_params) { + v.value().save(writer); + } else if (data_type == NodeValue::k_audio_params) { + TypeSerializer::save_audio_params(writer, v.value()); + } else { + writer->write_characters( + NodeValue::value_to_string(data_type, v, true)); + } + + writer->write_end_element(); // track + } + + writer->write_end_element(); // standard + + // Write keyframes + if (is_keyframing) { + writer->write_start_element("keyframes"); + + for (const NodeKeyframeTrack &track : + this->get_keyframe_tracks(input, element)) { + writer->write_start_element("track"); + + for (NodeKeyframe *key : track) { + writer->write_start_element("key"); + + key->save(writer, data_type); + + writer->write_end_element(); // key + } + + writer->write_end_element(); // track + } + + writer->write_end_element(); // keyframes + } + + if (data_type == NodeValue::k_color) { + // Save color management information + writer->write_text_element( + "csinput", + this->get_input_property(input, "col_input").to_string()); + writer->write_text_element( + "csdisplay", + this->get_input_property(input, "col_display").to_string()); + writer->write_text_element( + "csview", + this->get_input_property(input, "col_view").to_string()); + writer->write_text_element( + "cslook", + this->get_input_property(input, "col_look").to_string()); + } +} + +void Node::insert_input(const std::string &id, NodeValue::Type type, + const Variant &default_value, InputFlags flags, + int index) +{ + if (id.empty()) { + fprintf(stderr, "Rejected adding input with an empty ID on node %s\n", + this->id().c_str()); + return; + } + + if (has_param_with_id(id)) { + fprintf(stderr, + "Failed to add input to node %s - param with ID %s already exists\n", + this->id().c_str(), id.c_str()); + return; + } + + Node::Input i; + + i.type = type; + i.default_value = + NodeValue::split_normal_value_into_track_values(type, default_value); + i.flags = flags; + i.array_size = 0; + + input_ids_.insert(input_ids_.begin() + index, id); + input_data_.insert(input_data_.begin() + index, i); + + if (standard_immediates_.find(id) == standard_immediates_.end()) { + standard_immediates_[id] = create_immediate(id); + } +} + +void Node::remove_input(const std::string &id) +{ + int index = get_internal_input_index(id); + + if (index == -1) { + report_invalid_input("remove", id, -1); + return; + } + + input_ids_.erase(input_ids_.begin() + index); + input_data_.erase(input_data_.begin() + index); +} + +void Node::report_invalid_input(const char *attempted_action, const std::string &id, + int element) const +{ + fprintf(stderr, + "Failed to %s parameter %s element %d in node %s - input doesn't exist\n", + attempted_action, id.c_str(), element, this->id().c_str()); + +#ifndef _WIN32 + if (std::getenv("OAK_DEBUG_INVALID_INPUT")) { + void *frames[32]; + const int n = backtrace(frames, 32); + char **symbols = backtrace_symbols(frames, n); + if (symbols) { + for (int i = 0; i < n; i++) { + fprintf(stderr, "INVALID-INPUT-BT: %s\n", symbols[i]); + } + free(symbols); + } + } +#endif +} + +NodeInputImmediate *Node::create_immediate(const std::string &input) +{ + const Input *i = get_internal_input_data(input); + + if (i) { + return new NodeInputImmediate(i->type, i->default_value); + } else { + report_invalid_input("create immediate", input, -1); + return nullptr; + } +} + +void Node::array_resize_internal(const std::string &id, int size) +{ + Input *imm = get_internal_input_data(id); + + if (!imm) { + report_invalid_input("set array size", id, -1); + return; + } + + if (imm->array_size != size) { + // Update array size + if (imm->array_size < size) { + // Size is larger, create any immediates that don't exist + std::vector &subinputs = array_immediates_[id]; + for (int i = int(subinputs.size()); i < size; i++) { + subinputs.push_back(create_immediate(id)); + } + + // Note that we do not delete any immediates when decreasing size since the user might still + // want that data. Therefore it's important to note that array_size_ does NOT necessarily + // equal subinputs_.size() + } + + imm->array_size = size; + parameter_value_changed(id, -1, TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } +} + +std::string Node::get_connect_command_string(Node *output, const NodeInput &input) +{ + return "Connected " + output->get_label_and_name() + " to " + + input.node()->get_label_and_name() + " - " + input.get_input_name(); +} + +std::string Node::get_disconnect_command_string(Node *output, const NodeInput &input) +{ + return "Disconnected " + output->get_label_and_name() + " from " + + input.node()->get_label_and_name() + " - " + input.get_input_name(); +} + +int Node::get_internal_input_array_size(const std::string &input) +{ + auto it = array_immediates_.find(input); + return it != array_immediates_.end() ? int(it->second.size()) : 0; +} + +void find_ways_node_arrives_here_recursively(const Node *output, const Node *input, + std::vector &v) +{ + for (auto it = input->input_connections().cbegin(); + it != input->input_connections().cend(); it++) { + if (it->second == output) { + v.push_back(it->first); + } else { + find_ways_node_arrives_here_recursively(output, it->second, v); + } + } +} + +std::vector Node::find_ways_node_arrives_here(const Node *output) const +{ + std::vector v; + + find_ways_node_arrives_here_recursively(output, this, v); + + return v; +} + +void Node::set_input_name(const std::string &id, const std::string &name) +{ + Input *i = get_internal_input_data(id); + + if (i) { + i->human_name = name; + } else { + report_invalid_input("set name of", id, -1); + } +} + +const std::string &Node::get_label() const +{ + return label_; +} + +void Node::set_label(const std::string &s) +{ + if (label_ != s) { + label_ = s; + } +} + +std::string Node::get_label_and_name() const +{ + if (get_label().empty()) { + return name(); + } else { + return get_label() + " (" + name() + ")"; + } +} + +std::string Node::get_label_or_name() const +{ + if (get_label().empty()) { + return name(); + } + return get_label(); +} + +void Node::copy_inputs(const Node *source, Node *destination, + bool include_connections, MultiUndoCommand *command) +{ + assert(source->id() == destination->id()); + + for (const std::string &input : source->inputs()) { + // NOTE: This assert is to ensure that inputs in the source also exist in the destination, which + // they should. If they don't and you hit this assert, check if you're handling group + // passthroughs correctly. + assert(destination->has_input_with_id(input)); + + copy_input(source, destination, input, include_connections, true, + command); + } + + if (command) { + command->add_child( + new NodeRenameCommand(destination, source->get_label())); + } else { + destination->set_label(source->get_label()); + } + + if (command) { + command->add_child(new NodeOverrideColorCommand( + destination, source->get_override_color())); + } else { + destination->set_override_color(source->get_override_color()); + } +} + +void Node::copy_input(const Node *src, Node *dst, const std::string &input, + bool include_connections, bool traverse_arrays, + MultiUndoCommand *command) +{ + assert(src->id() == dst->id()); + + copy_values_of_element(src, dst, input, -1, command); + + // Copy array size + if (src->input_is_array(input) && traverse_arrays) { + int src_array_sz = src->input_array_size(input); + + for (int i = 0; i < src_array_sz; i++) { + copy_values_of_element(src, dst, input, i, command); + } + } + + // Copy connections + if (include_connections) { + // Copy all connections + for (auto it = src->input_connections().cbegin(); + it != src->input_connections().cend(); it++) { + if (!traverse_arrays && it->first.element() != -1) { + continue; + } + + auto conn_output = it->second; + NodeInput conn_input(dst, input, it->first.element()); + + if (command) { + command->add_child( + new NodeEdgeAddCommand(conn_output, conn_input)); + } else { + connect_edge(conn_output, conn_input); + } + } + } +} + +void Node::copy_values_of_element(const Node *src, Node *dst, const std::string &input, + int src_element, int dst_element, + MultiUndoCommand *command) +{ + if (dst_element >= dst->get_internal_input_array_size(input)) { + fprintf(stderr, + "Ignored destination element that was out of array bounds\n"); + return; + } + + NodeInput dst_input(dst, input, dst_element); + + // Copy standard value + SplitValue standard = src->get_split_standard_value(input, src_element); + if (command) { + command->add_child( + new NodeParamSetSplitStandardValueCommand(dst_input, standard)); + } else { + dst->set_split_standard_value(input, standard, dst_element); + } + + // Copy keyframes + if (NodeInputImmediate *immediate = dst->get_immediate(input, dst_element)) { + if (command) { + command->add_child( + new NodeImmediateRemoveAllKeyframesCommand(immediate)); + } else { + immediate->delete_all_keyframes(); + } + } + + for (const NodeKeyframeTrack &track : + src->get_immediate(input, src_element)->keyframe_tracks()) { + for (NodeKeyframe *key : track) { + NodeKeyframe *copy = key->copy(dst_element, command ? nullptr : dst); + if (command) { + command->add_child( + new NodeParamInsertKeyframeCommand(dst, copy)); + } else { + dst->add_keyframe(copy); + } + } + } + + // Copy keyframing state + if (src->is_input_keyframable(input)) { + bool is_keying = src->is_input_keyframing(input, src_element); + if (command) { + command->add_child( + new NodeParamSetKeyframingCommand(dst_input, is_keying)); + } else { + dst->set_input_is_keyframing(input, is_keying, dst_element); + } + } + + // If this is the root of an array, copy the array size + if (src_element == -1 && dst_element == -1) { + int array_sz = src->input_array_size(input); + if (command) { + command->add_child( + new NodeArrayResizeCommand(dst, input, array_sz)); + } else { + dst->array_resize_internal(input, array_sz); + } + } + + // Copy value hint + Node::ValueHint vh = src->get_value_hint_for_input(input, src_element); + if (command) { + command->add_child(new NodeSetValueHintCommand(dst_input, vh)); + } else { + dst->set_value_hint_for_input(input, vh, dst_element); + } +} + +void get_dependencies_recursively(std::vector &list, const Node *node, + bool traverse, bool exclusive_only) +{ + for (auto it = node->input_connections().cbegin(); + it != node->input_connections().cend(); it++) { + Node *connected_node = it->second; + + if (!exclusive_only || !connected_node->is_item()) { + if (std::find(list.begin(), list.end(), connected_node) == + list.end()) { + list.push_back(connected_node); + + if (traverse) { + get_dependencies_recursively(list, connected_node, traverse, + exclusive_only); + } + } + } + } +} + +/** + * @brief Recursively collects dependencies of Node `n` and appends them to `list` + * + * @param traverse + * + * TRUE to recursively traverse each node for a complete dependency graph. FALSE to return only the immediate + * dependencies. + */ +std::vector Node::get_dependencies_internal(bool traverse, + bool exclusive_only) const +{ + std::vector list; + + get_dependencies_recursively(list, this, traverse, exclusive_only); + + return list; +} + +std::vector Node::get_dependencies() const +{ + return get_dependencies_internal(true, false); +} + +std::vector Node::get_exclusive_dependencies() const +{ + return get_dependencies_internal(true, true); +} + +std::vector Node::get_immediate_dependencies() const +{ + return get_dependencies_internal(false, false); +} + +ShaderCode Node::get_shader_code(const ShaderRequest &request) const +{ + return ShaderCode(std::string(), std::string()); +} + +void Node::process_samples(const NodeValueRow &, const SampleBuffer &, + SampleBuffer &, int) const +{ +} + +void Node::generate_frame(FramePtr frame, const GenerateJob &job) const +{ + (void)frame; + (void)job; +} + +bool Node::inputs_from(Node *n, bool recursively) const +{ + for (auto it = input_connections_.cbegin(); it != input_connections_.cend(); + it++) { + Node *connected = it->second; + + if (connected == n) { + return true; + } else if (recursively && connected->inputs_from(n, recursively)) { + return true; + } + } + + return false; +} + +bool Node::inputs_from(const std::string &id, bool recursively) const +{ + for (auto it = input_connections_.cbegin(); it != input_connections_.cend(); + it++) { + Node *connected = it->second; + + if (connected->id() == id) { + return true; + } else if (recursively && connected->inputs_from(id, recursively)) { + return true; + } + } + + return false; +} + +void Node::disconnect_all() +{ + // During project teardown, skip the disconnect events/signals: the + // other side's handlers touch this node's members which are already + // destroyed at ~Node time (e.g. caches), which is a use-after-free. + const bool silent = project() && project()->is_being_cleared(); + + // Disconnect inputs (copy map since internal map will change as we disconnect) + InputConnections copy = input_connections_; + for (auto it = copy.cbegin(); it != copy.cend(); it++) { + disconnect_edge(it->second, it->first, silent); + } + + while (!output_connections_.empty()) { + OutputConnection conn = output_connections_.back(); + disconnect_edge(conn.first, conn.second, silent); + } +} + +std::string Node::get_category_name(const CategoryID &c) +{ + switch (c) { + case k_category_output: + return "Output"; + case k_category_distort: + return "Distort"; + case k_category_math: + return "Math"; + case k_category_keying: + return "Keying"; + case k_category_color: + return "Color"; + case k_category_filter: + return "Filter"; + case k_category_timeline: + return "Timeline"; + case k_category_generator: + return "Generator"; + case k_category_transition: + return "Transition"; + case k_category_project: + return "Project"; + case k_category_open_fx: + return "OpenFX"; + case k_category_time: + return "Time"; + case k_category_unknown: + case k_category_count: + break; + } + + return "Uncategorized"; +} + +TimeRange Node::transform_time_to(TimeRange time, Node *target, + TransformTimeDirection dir, int path_index) +{ + Node *from = this; + Node *to = target; + + if (dir == k_towards_input) { + std::swap(from, to); + } + + std::list path = find_path(from, to, path_index); + + if (!path.empty()) { + if (dir == k_towards_input) { + for (auto it = path.crbegin(); it != path.crend(); it++) { + const NodeInput &i = (*it); + time = i.node()->input_time_adjustment(i.input(), i.element(), + time, false); + } + } else { + // Traverse in output direction + for (auto it = path.cbegin(); it != path.cend(); it++) { + const NodeInput &i = (*it); + time = i.node()->output_time_adjustment(i.input(), i.element(), + time); + } + } + } + + return time; +} + +void Node::parameter_value_changed(const std::string &input, int element, + const TimeRange &range) +{ + InputValueChangedEvent(input, element); + + if (get_input_flags(input) & k_input_flag_ignore_invalidations) { + return; + } + + invalidate_cache(range, input, element); +} + +TimeRange Node::get_range_affected_by_keyframe(NodeKeyframe *key) const +{ + const NodeKeyframeTrack &key_track = get_track_from_keyframe(key); + int keyframe_index = int( + std::find(key_track.begin(), key_track.end(), key) - key_track.begin()); + + TimeRange range = get_range_around_index(key->input(), keyframe_index, + key->track(), key->element()); + + // If a previous key exists and it's a hold, we don't need to invalidate those frames + if (key_track.size() > 1 && keyframe_index > 0 && + key_track.at(keyframe_index - 1)->type() == NodeKeyframe::k_hold) { + range.set_in(key->time()); + } + + return range; +} + +TimeRange Node::get_range_around_index(const std::string &input, int index, int track, + int element) const +{ + Rational range_begin = RATIONAL_MIN; + Rational range_end = RATIONAL_MAX; + + const NodeKeyframeTrack &key_track = + get_immediate(input, element)->keyframe_tracks().at(track); + + if (key_track.size() > 1) { + if (index > 0) { + // If this is not the first key, we'll need to limit it to the key just before + range_begin = key_track.at(index - 1)->time(); + } + if (index < int(key_track.size()) - 1) { + // If this is not the last key, we'll need to limit it to the key just after + range_end = key_track.at(index + 1)->time(); + } + } + + return TimeRange(range_begin, range_end); +} + +void Node::clear_element(const std::string &input, int index) +{ + get_immediate(input, index)->delete_all_keyframes(); + + if (is_input_keyframable(input)) { + set_input_is_keyframing(input, false, index); + } + + set_split_standard_value(input, get_split_default_value(input), index); +} + +void Node::InputValueChangedEvent(const std::string &input, int element) +{ + (void)input; + (void)element; +} + +void Node::InputConnectedEvent(const std::string &input, int element, Node *output) +{ + (void)input; + (void)element; + (void)output; +} + +void Node::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + (void)input; + (void)element; + (void)output; +} + +void Node::OutputConnectedEvent(const NodeInput &input) +{ + (void)input; +} + +void Node::OutputDisconnectedEvent(const NodeInput &input) +{ + (void)input; +} + +void Node::add_keyframe(NodeKeyframe *key) +{ + // Formerly the ChildAdded branch of childEvent(): register with the + // input immediate (insertion sorted) and invalidate the affected range. + key->set_parent(this); + + get_immediate(key->input(), key->element())->insert_keyframe(key); + + parameter_value_changed(NodeInput(this, key->input(), key->element()), + get_range_affected_by_keyframe(key)); +} + +void Node::remove_keyframe(NodeKeyframe *key) +{ + // Formerly the ChildRemoved branch of childEvent(): invalidate the + // affected range and unregister from the input immediate. + TimeRange time_affected = get_range_affected_by_keyframe(key); + + get_immediate(key->input(), key->element())->remove_keyframe(key); + + key->set_parent(nullptr); + + parameter_value_changed(NodeInput(this, key->input(), key->element()), + time_affected); +} + +void Node::invalidate_from_keyframe_bezier_in_change(NodeKeyframe *key) +{ + const NodeKeyframeTrack &track = get_track_from_keyframe(key); + int keyframe_index = int( + std::find(track.begin(), track.end(), key) - track.begin()); + + Rational start = RATIONAL_MIN; + Rational end = key->time(); + + if (keyframe_index > 0) { + start = track.at(keyframe_index - 1)->time(); + } + + parameter_value_changed(key->key_track_ref().input(), TimeRange(start, end)); +} + +void Node::invalidate_from_keyframe_bezier_out_change(NodeKeyframe *key) +{ + const NodeKeyframeTrack &track = get_track_from_keyframe(key); + int keyframe_index = int( + std::find(track.begin(), track.end(), key) - track.begin()); + + Rational start = key->time(); + Rational end = RATIONAL_MAX; + + if (keyframe_index < int(track.size()) - 1) { + end = track.at(keyframe_index + 1)->time(); + } + + parameter_value_changed(key->key_track_ref().input(), TimeRange(start, end)); +} + +void Node::invalidate_from_keyframe_time_change(NodeKeyframe *key) +{ + NodeInputImmediate *immediate = get_immediate(key->input(), key->element()); + TimeRange original_range = get_range_affected_by_keyframe(key); + + core::TimeRangeList invalidate_range; + invalidate_range.insert(original_range); + + if (!(original_range.in() < key->time() && + original_range.out() > key->time())) { + // This keyframe needs resorting, store it and remove it from the list + immediate->remove_keyframe(key); + + // Automatically insertion sort + immediate->insert_keyframe(key); + + // Invalidate new area that the keyframe has been moved to + invalidate_range.insert(get_range_affected_by_keyframe(key)); + } + + // Invalidate entire area surrounding the keyframe (either where it currently is, or where it used to be before it + // was resorted in the if block above) + for (const TimeRange &r : invalidate_range) { + parameter_value_changed(key->key_track_ref().input(), r); + } +} + +void Node::invalidate_from_keyframe_value_change(NodeKeyframe *key) +{ + parameter_value_changed(key->key_track_ref().input(), + get_range_affected_by_keyframe(key)); +} + +void Node::invalidate_from_keyframe_type_changed(NodeKeyframe *key) +{ + const NodeKeyframeTrack &track = get_track_from_keyframe(key); + + if (track.size() == 1) { + // If there are no other frames, the interpolation won't do anything + return; + } + + // Invalidate entire range + parameter_value_changed( + key->key_track_ref().input(), + get_range_around_index(key->input(), + int(std::find(track.begin(), track.end(), key) - + track.begin()), + key->track(), key->element())); +} + +void Node::set_value_at_time(const NodeInput &input, const Rational &time, + const Variant &value, int track, + MultiUndoCommand *command, + bool insert_on_all_tracks_if_no_key) +{ + if (input.is_keyframing()) { + Rational node_time = time; + + NodeKeyframe *existing_key = + input.get_keyframe_at_time_on_track(node_time, track); + + if (existing_key) { + command->add_child( + new NodeParamSetKeyframeValueCommand(existing_key, value)); + } else { + // No existing key, create a new one + int nb_tracks = NodeValue::get_number_of_keyframe_tracks( + input.node()->get_input_data_type(input.input())); + for (int i = 0; i < nb_tracks; i++) { + Variant track_value; + + if (i == track) { + track_value = value; + } else if (!insert_on_all_tracks_if_no_key) { + continue; + } else { + track_value = input.node()->get_split_value_at_time_on_track( + input.input(), node_time, i, input.element()); + } + + NodeKeyframe *new_key = new NodeKeyframe( + node_time, track_value, + input.node()->get_best_keyframe_type_for_time_on_track( + NodeKeyframeTrackReference(input, i), node_time), + i, input.element(), input.input()); + + command->add_child( + new NodeParamInsertKeyframeCommand(input.node(), new_key)); + } + } + } else { + command->add_child(new NodeParamSetStandardValueCommand( + NodeKeyframeTrackReference(input, track), value)); + } +} + +bool find_path_internal(std::list &vec, Node *from, Node *to, + int &path_index) +{ + for (auto it = from->output_connections().cbegin(); + it != from->output_connections().cend(); it++) { + const NodeInput &next = it->second; + + vec.push_back(next); + + if (next.node() == to) { + // Found a path! Determine if it's the index we want + if (path_index == 0) { + // It is! + return true; + } else { + // It isn't, keep looking... + path_index--; + } + } + + if (find_path_internal(vec, next.node(), to, path_index)) { + return true; + } + + vec.pop_back(); + } + + return false; +} + +std::list Node::find_path(Node *from, Node *to, int path_index) +{ + std::list v; + + find_path_internal(v, from, to, path_index); + + return v; +} + +bool Node::ValueHint::load(XmlStreamReader *reader) +{ + unsigned int version = 0; + for (const XmlStreamAttribute &attr : reader->attributes()) { + version = unsigned(strtoul(attr.value.c_str(), nullptr, 10)); + } + + (void)version; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "types") { + std::vector types; + while (xml_read_next_start_element(reader)) { + if (reader->name() == "type") { + types.push_back(static_cast( + strtol(reader->read_element_text().c_str(), nullptr, + 10))); + } else { + reader->skip_current_element(); + } + } + this->set_type(types); + } else if (reader->name() == "index") { + this->set_index( + int(strtol(reader->read_element_text().c_str(), nullptr, 10))); + } else if (reader->name() == "tag") { + this->set_tag(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void Node::ValueHint::save(XmlStreamWriter *writer) const +{ + writer->write_attribute("version", std::to_string(1)); + + writer->write_start_element("types"); + + for (auto it = this->types().cbegin(); it != this->types().cend(); it++) { + writer->write_text_element("type", std::to_string(int(*it))); + } + + writer->write_end_element(); // types + + writer->write_text_element("index", std::to_string(this->index())); + + writer->write_text_element("tag", this->tag()); +} + +bool Node::Position::load(XmlStreamReader *reader) +{ + bool got_pos_x = false; + bool got_pos_y = false; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "x") { + this->position.set_x( + strtod(reader->read_element_text().c_str(), nullptr)); + got_pos_x = true; + } else if (reader->name() == "y") { + this->position.set_y( + strtod(reader->read_element_text().c_str(), nullptr)); + got_pos_y = true; + } else if (reader->name() == "expanded") { + this->expanded = + strtol(reader->read_element_text().c_str(), nullptr, 10); + } else { + reader->skip_current_element(); + } + } + + return got_pos_x && got_pos_y; +} + +void Node::Position::save(XmlStreamWriter *writer) const +{ + writer->write_text_element("x", number_to_string(this->position.x())); + writer->write_text_element("y", number_to_string(this->position.y())); + writer->write_text_element("expanded", std::to_string(int(this->expanded))); +} + +} diff --git a/src/node/src/node.h b/src/node/src/node.h new file mode 100644 index 000000000..73a7b71e9 --- /dev/null +++ b/src/node/src/node.h @@ -0,0 +1,1571 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODE_H +#define OAK_NODE_H + +#include "ofxhImageEffectAPI.h" + +#include +#include +#include +#include +#include +#include + +#include "codec/frame.h" +#include "gizmo/draggable.h" +#include "globals.h" +#include "keyframe.h" +#include "inputimmediate.h" +#include "param.h" +#include "olive/core/util/timerange.h" +#include "render/audioplaybackcache.h" +#include "render/audiowaveformcache.h" +#include "render/framehashcache.h" +#include "render/job/generatejob.h" +#include "render/job/samplejob.h" +#include "render/job/shaderjob.h" +#include "render/shadercode.h" +#include "splitvalue.h" +#include "undocommand.h" +#include "xmlutils.h" + +namespace olive +{ + +using core::TimeRange; + +#define NODE_DEFAULT_FUNCTIONS(x) \ + NODE_DEFAULT_DESTRUCTOR(x) \ + NODE_COPY_FUNCTION(x) + +#define NODE_DEFAULT_DESTRUCTOR(x) \ + virtual ~x() override \ + { \ + disconnect_all(); \ + } + +#define NODE_COPY_FUNCTION(x) \ + virtual Node *copy() const override \ + { \ + return new x(); \ + } + +class Folder; +class Project; +struct SerializedData; + +/** + * @brief A single processing unit that can be connected with others to create intricate processing systems + * + * A cornerstone of "visual programming", a node is a single "function" that takes input and returns an output that can + * be connected to other nodes. Inputs can be either user-set or retrieved from the output of another node. By joining + * several nodes together, intricate, highly customizable, and infinitely extensible systems can be made for processing + * data. It can also all be exposed to the user without forcing them to write code or compile anything. + * + * A major example in Olive is the entire rendering workflow. To render a frame, Olive will work through a node graph + * that can be infinitely customized by the user to create images. + * + * This is a simple base class designed to contain all the functionality for this kind of processing connective unit. + * It is an abstract class intended to be subclassed to create nodes with actual functionality. + */ +class Node { +public: + enum CategoryID { + k_category_unknown = -1, + + k_category_output, + k_category_generator, + k_category_math, + k_category_keying, + k_category_filter, + k_category_color, + k_category_time, + k_category_timeline, + k_category_transition, + k_category_distort, + k_category_project, + k_category_open_fx, + + k_category_count + }; + + enum Flag { + k_none = 0, + k_dont_show_in_param_view = 0x1, + k_video_effect = 0x2, + k_audio_effect = 0x4, + k_dont_show_in_create_menu = 0x8, + k_is_item = 0x10 + }; + + struct ContextPair { + Node *node; + Node *context; + }; + + Node(); + + virtual ~Node(); + + /** + * @brief Creates a clone of the Node + * + * By default, the clone will NOT have the values and connections of the original node. The caller is responsible for + * copying that data with functions like CopyInputs() as copies may be done for different reasons. + */ + virtual Node *copy() const = 0; + + /** + * @brief Convenience function - returns the graph this node was added to + */ + Project *parent() const + { + return parent_; + } + + /** + * @brief Set the graph this node belongs to (replaces QObject::setParent) + */ + void set_parent(Project *p) + { + parent_ = p; + } + + Project *project() const + { + return parent_; + } + + const uint64_t &get_flags() const + { + return flags_; + } + + /** + * @brief Return the name of the node + * + * This is the node's name shown to the user. This must be overridden by subclasses, and preferably run through the + * translator. + */ + virtual std::string name() const = 0; + + /** + * @brief Returns a shortened name of this node if applicable + * + * Defaults to returning Name() but can be overridden. + */ + virtual std::string short_name() const; + + /** + * @brief Return the unique identifier of the node + * + * This is used in save files and any other times a specific node must be picked out at runtime. This must be an ID + * completely unique to this node, and preferably in bundle identifier format (e.g. "org.company.Name"). This string + * should NOT be translated. + */ + virtual std::string id() const = 0; + + /** + * @brief Return the category this node is in (optional for subclassing, but recommended) + * + * In any organized node menus, show the node in this category. If this node should be in a subfolder of a subfolder, + * use a "/" to separate categories (e.g. "Distort/Noise"). The string should not start with a "/" as this will be + * interpreted as an empty string category. This value should be run through a translator as its largely user + * oriented. + */ + virtual std::vector category() const = 0; + + /** + * @brief Return a sub-category string for secondary grouping + * within the primary category (e.g. "Filter" under "OpenFX"). + */ + virtual std::string sub_category() const + { + return std::string(); + } + + /** + * @brief Return a description of this node's purpose (optional for subclassing, but recommended) + * + * A short (1-2 sentence) description of what this node should do to help the user understand its purpose. This should + * be run through a translator. + */ + virtual std::string description() const; + + Folder *folder() const + { + return folder_; + } + + bool is_item() const + { + return flags_ & k_is_item; + } + + /** + * @brief Function called to retranslate parameter names (should be overridden in derivatives) + */ + virtual void retranslate(); + + enum DataType { + icon, + duration, + created_time, + modified_time, + frequency_rate, + tooltip + }; + + virtual Variant data(const DataType &d) const; + + const std::vector &inputs() const + { + return input_ids_; + } + + virtual std::vector ignore_inputs_for_rendering() const + { + return std::vector(); + } + + class ActiveElements { + public: + enum Mode { k_all_elements, k_specified, k_no_elements }; + + ActiveElements(Mode m = k_all_elements) + { + mode_ = m; + } + + Mode mode() const + { + return mode_; + } + std::list elements() const + { + return elements_; + } + + void add(int e) + { + elements_.push_back(e); + mode_ = k_specified; + } + + private: + Mode mode_; + std::list elements_; + }; + + virtual ActiveElements get_active_elements_at_time(const std::string &input, + const TimeRange &r) const + { + return ActiveElements::k_all_elements; + } + + bool has_input_with_id(const std::string &id) const + { + return std::find(input_ids_.begin(), input_ids_.end(), id) != + input_ids_.end(); + } + + bool has_param_with_id(const std::string &id) const + { + return has_input_with_id(id); + } + + FrameHashCache *video_frame_cache() const + { + return video_cache_; + } + + ThumbnailCache *thumbnail_cache() const + { + return thumbnail_cache_; + } + + AudioPlaybackCache *audio_playback_cache() const + { + return audio_cache_; + } + + AudioWaveformCache *waveform_cache() const + { + return waveform_cache_; + } + + virtual TimeRange get_video_cache_range() const + { + return TimeRange(); + } + virtual TimeRange get_audio_cache_range() const + { + return TimeRange(); + } + + struct Position { + Position(const PointF &p = PointF(0, 0), bool e = false) + { + position = p; + expanded = e; + } + + bool load(XmlStreamReader *reader); + void save(XmlStreamWriter *writer) const; + + PointF position; + bool expanded; + + inline Position &operator+=(const Position &p) + { + position += p.position; + return *this; + } + + inline Position &operator-=(const Position &p) + { + position -= p.position; + return *this; + } + + friend inline const Position operator+(Position a, const Position &b) + { + a += b; + return a; + } + + friend inline const Position operator-(Position a, const Position &b) + { + a -= b; + return a; + } + }; + + using PositionMap = std::map; + const PositionMap &get_context_positions() const + { + return context_positions_; + } + + bool is_node_expanded_in_context(Node *node) const + { + auto it = context_positions_.find(node); + return it != context_positions_.end() && it->second.expanded; + } + + bool context_contains_node(Node *node) const + { + return context_positions_.count(node); + } + + Position get_node_position_data_in_context(Node *node) + { + auto it = context_positions_.find(node); + return it != context_positions_.end() ? it->second : Position(); + } + + PointF get_node_position_in_context(Node *node) + { + return get_node_position_data_in_context(node).position; + } + + bool set_node_position_in_context(Node *node, const PointF &pos); + + bool set_node_position_in_context(Node *node, const Position &pos); + + void set_node_expanded_in_context(Node *node, bool e) + { + context_positions_[node].expanded = e; + } + + bool remove_node_from_context(Node *node); + + /** + * @brief Retrieve the color of this node + */ + Color color() const; + + int get_override_color() const + { + return override_color_; + } + + /** + * @brief Sets the override color. Set to -1 for no override color. + */ + void set_override_color(int index) + { + if (override_color_ != index) { + override_color_ = index; + } + } + + static void connect_edge(Node *output, const NodeInput &input); + + static void disconnect_edge(Node *output, const NodeInput &input, + bool silent = false); + + void copy_cache_uuids_from(Node *n); + + bool are_caches_enabled() const + { + return caches_enabled_; + } + void set_caches_enabled(bool e) + { + caches_enabled_ = e; + } + + virtual std::string get_input_name(const std::string &id) const; + + void set_input_name(const std::string &id, const std::string &name); + + bool is_input_hidden(const std::string &input) const; + bool is_input_connectable(const std::string &input) const; + bool is_input_keyframable(const std::string &input) const; + + bool is_input_keyframing(const std::string &input, int element = -1) const; + bool is_input_keyframing(const NodeInput &input) const + { + return is_input_keyframing(input.input(), input.element()); + } + + void set_input_is_keyframing(const std::string &input, bool e, int element = -1); + void set_input_is_keyframing(const NodeInput &input, bool e) + { + set_input_is_keyframing(input.input(), e, input.element()); + } + + bool is_input_connected(const std::string &input, int element = -1) const; + bool is_input_connected(const NodeInput &input) const + { + return is_input_connected(input.input(), input.element()); + } + + virtual bool is_input_connected_for_render(const std::string &input, + int element = -1) const + { + return is_input_connected(input, element); + } + bool is_input_connected_for_render(const NodeInput &input) const + { + return is_input_connected_for_render(input.input(), input.element()); + } + + bool is_input_static(const std::string &input, int element = -1) const + { + return !is_input_connected(input, element) && + !is_input_keyframing(input, element); + } + + bool is_input_static(const NodeInput &input) const + { + return is_input_static(input.input(), input.element()); + } + + Node *get_connected_output(const std::string &input, int element = -1) const; + + Node *get_connected_output(const NodeInput &input) const + { + return get_connected_output(input.input(), input.element()); + } + + virtual Node *get_connected_render_output(const std::string &input, + int element = -1) const + { + return get_connected_output(input, element); + } + + Node *get_connected_render_output(const NodeInput &input) const + { + return get_connected_render_output(input.input(), input.element()); + } + + bool is_using_standard_value(const std::string &input, int track, + int element = -1) const; + + NodeValue::Type get_input_data_type(const std::string &id) const; + void set_input_data_type(const std::string &id, const NodeValue::Type &type); + + bool has_input_property(const std::string &id, const std::string &name) const; + std::map get_input_properties(const std::string &id) const; + Variant get_input_property(const std::string &id, const std::string &name) const; + void set_input_property(const std::string &id, const std::string &name, + const Variant &value); + + Variant get_value_at_time(const std::string &input, const Rational &time, + int element = -1) const + { + NodeValue::Type type = get_input_data_type(input); + + return NodeValue::combine_track_values_into_normal_value( + type, get_split_value_at_time(input, time, element)); + } + + Variant get_value_at_time(const NodeInput &input, const Rational &time) + { + return get_value_at_time(input.input(), time, input.element()); + } + + SplitValue get_split_value_at_time(const std::string &input, const Rational &time, + int element = -1) const; + + SplitValue get_split_value_at_time(const NodeInput &input, const Rational &time) + { + return get_split_value_at_time(input.input(), time, input.element()); + } + + Variant get_split_value_at_time_on_track(const std::string &input, + const Rational &time, int track, + int element = -1) const; + Variant get_split_value_at_time_on_track(const NodeInput &input, + const Rational &time, int track) const + { + return get_split_value_at_time_on_track(input.input(), time, track, + input.element()); + } + + Variant get_split_value_at_time_on_track(const NodeKeyframeTrackReference &input, + const Rational &time) const + { + return get_split_value_at_time_on_track(input.input(), time, input.track()); + } + + Variant get_default_value(const std::string &input) const; + SplitValue get_split_default_value(const std::string &input) const; + Variant get_split_default_value_on_track(const std::string &input, int track) const; + + void set_default_value(const std::string &input, const Variant &val); + void set_split_default_value(const std::string &input, const SplitValue &val); + void set_split_default_value_on_track(const std::string &input, const Variant &val, + int track); + + const std::vector &get_keyframe_tracks(const std::string &input, + int element) const; + const std::vector & + get_keyframe_tracks(const NodeInput &input) const + { + return get_keyframe_tracks(input.input(), input.element()); + } + + std::vector get_keyframes_at_time(const std::string &input, + const Rational &time, + int element = -1) const; + std::vector get_keyframes_at_time(const NodeInput &input, + const Rational &time) const + { + return get_keyframes_at_time(input.input(), time, input.element()); + } + + NodeKeyframe *get_keyframe_at_time_on_track(const std::string &input, + const Rational &time, int track, + int element = -1) const; + NodeKeyframe *get_keyframe_at_time_on_track(const NodeInput &input, + const Rational &time, + int track) const + { + return get_keyframe_at_time_on_track(input.input(), time, track, + input.element()); + } + + NodeKeyframe * + get_keyframe_at_time_on_track(const NodeKeyframeTrackReference &input, + const Rational &time) const + { + return get_keyframe_at_time_on_track(input.input(), time, input.track()); + } + + NodeKeyframe::Type + get_best_keyframe_type_for_time_on_track(const std::string &input, + const Rational &time, int track, + int element = -1) const; + + NodeKeyframe::Type get_best_keyframe_type_for_time_on_track(const NodeInput &input, + const Rational &time, + int track) const + { + return get_best_keyframe_type_for_time_on_track(input.input(), time, track, + input.element()); + } + + NodeKeyframe::Type + get_best_keyframe_type_for_time_on_track(const NodeKeyframeTrackReference &input, + const Rational &time) const + { + return get_best_keyframe_type_for_time_on_track(input.input(), time, + input.track()); + } + + int get_number_of_keyframe_tracks(const std::string &id) const; + int get_number_of_keyframe_tracks(const NodeInput &id) const + { + return get_number_of_keyframe_tracks(id.input()); + } + + NodeKeyframe *get_earliest_keyframe(const std::string &id, + int element = -1) const; + NodeKeyframe *get_earliest_keyframe(const NodeInput &id) const + { + return get_earliest_keyframe(id.input(), id.element()); + } + + NodeKeyframe *get_latest_keyframe(const std::string &id, int element = -1) const; + NodeKeyframe *get_latest_keyframe(const NodeInput &id) const + { + return get_latest_keyframe(id.input(), id.element()); + } + + NodeKeyframe *get_closest_keyframe_before_time(const std::string &id, + const Rational &time, + int element = -1) const; + NodeKeyframe *get_closest_keyframe_before_time(const NodeInput &id, + const Rational &time) const + { + return get_closest_keyframe_before_time(id.input(), time, id.element()); + } + + NodeKeyframe *get_closest_keyframe_after_time(const std::string &id, + const Rational &time, + int element = -1) const; + NodeKeyframe *get_closest_keyframe_after_time(const NodeInput &id, + const Rational &time) const + { + return get_closest_keyframe_after_time(id.input(), time, id.element()); + } + + bool has_keyframe_at_time(const std::string &id, const Rational &time, + int element = -1) const; + bool has_keyframe_at_time(const NodeInput &id, const Rational &time) const + { + return has_keyframe_at_time(id.input(), time, id.element()); + } + + StringList get_combo_box_strings(const std::string &id) const; + + Variant get_standard_value(const std::string &id, int element = -1) const; + Variant get_standard_value(const NodeInput &id) const + { + return get_standard_value(id.input(), id.element()); + } + + SplitValue get_split_standard_value(const std::string &id, int element = -1) const; + SplitValue get_split_standard_value(const NodeInput &id) const + { + return get_split_standard_value(id.input(), id.element()); + } + + Variant get_split_standard_value_on_track(const std::string &input, int track, + int element = -1) const; + Variant + get_split_standard_value_on_track(const NodeKeyframeTrackReference &id) const + { + return get_split_standard_value_on_track(id.input().input(), id.track(), + id.input().element()); + } + + void set_standard_value(const std::string &id, const Variant &value, + int element = -1); + void set_standard_value(const NodeInput &id, const Variant &value) + { + set_standard_value(id.input(), value, id.element()); + } + + void set_split_standard_value(const std::string &id, const SplitValue &value, + int element = -1); + void set_split_standard_value(const NodeInput &id, const SplitValue &value) + { + set_split_standard_value(id.input(), value, id.element()); + } + + void set_split_standard_value_on_track(const std::string &id, int track, + const Variant &value, int element = -1); + void set_split_standard_value_on_track(const NodeKeyframeTrackReference &id, + const Variant &value) + { + set_split_standard_value_on_track(id.input().input(), id.track(), value, + id.input().element()); + } + + bool input_is_array(const std::string &id) const; + + void input_array_insert(const std::string &id, int index); + void input_array_resize(const std::string &id, int size); + void input_array_remove(const std::string &id, int index); + + void input_array_append(const std::string &id) + { + input_array_resize(id, input_array_size(id) + 1); + } + + void input_array_prepend(const std::string &id) + { + input_array_insert(id, 0); + } + + void input_array_remove_last(const std::string &id) + { + input_array_resize(id, input_array_size(id) - 1); + } + + int input_array_size(const std::string &id) const; + + NodeInputImmediate *get_immediate(const std::string &input, int element) const; + + NodeInput get_effect_input() + { + return effect_input_.empty() ? NodeInput() : + NodeInput(this, effect_input_); + } + + const std::string &get_effect_input_id() const + { + return effect_input_; + } + + class ValueHint { + public: + explicit ValueHint( + const std::vector &types = std::vector(), + int index = -1, const std::string &tag = std::string()) + : type_(types) + , index_(index) + , tag_(tag) + { + } + + explicit ValueHint(const std::vector &types, + const std::string &tag) + : type_(types) + , index_(-1) + , tag_(tag) + { + } + + explicit ValueHint(int index) + : index_(index) + { + } + + explicit ValueHint(const std::string &tag) + : index_(-1) + , tag_(tag) + { + } + + const std::vector &types() const + { + return type_; + } + const int &index() const + { + return index_; + } + const std::string &tag() const + { + return tag_; + } + + void set_type(const std::vector &type) + { + type_ = type; + } + void set_index(const int &index) + { + index_ = index; + } + void set_tag(const std::string &tag) + { + tag_ = tag; + } + + bool load(XmlStreamReader *reader); + void save(XmlStreamWriter *writer) const; + + private: + std::vector type_; + int index_; + std::string tag_; + }; + + const std::map &get_value_hints() const + { + return value_hints_; + } + + virtual ValueHint get_value_hint_for_input(const std::string &input, + int element = -1) const + { + auto it = value_hints_.find({ input, element }); + return it != value_hints_.end() ? it->second : ValueHint(); + } + + void set_value_hint_for_input(const std::string &input, const ValueHint &hint, + int element = -1); + + const NodeKeyframeTrack &get_track_from_keyframe(NodeKeyframe *key) const; + + using InputConnections = std::map; + + /** + * @brief Return map of input connections + * + * Inputs can only have one connection, so the key is the input connected and the value is the + * output that it's connected to. + */ + const InputConnections &input_connections() const + { + return input_connections_; + } + + using OutputConnection = std::pair; + using OutputConnections = std::vector; + + /** + * @brief Return list of output connections + * + * An output can connect an infinite amount of inputs, so in this map, the key is the output and + * the value is a vector of inputs. + */ + const OutputConnections &output_connections() const + { + return output_connections_; + } + + /** + * @brief Return a list of all Nodes that this Node's inputs are connected to (does not include this Node) + */ + std::vector get_dependencies() const; + + /** + * @brief Returns a list of Nodes that this Node is dependent on, provided no other Nodes are dependent on them + * outside of this hierarchy. + * + * Similar to GetDependencies(), but excludes any Nodes that are used outside the dependency graph of this Node. + */ + std::vector get_exclusive_dependencies() const; + + /** + * @brief Retrieve immediate dependencies (only nodes that are directly connected to the inputs of this one) + */ + std::vector get_immediate_dependencies() const; + + struct ShaderRequest { + ShaderRequest(const std::string &shader_id) + { + id = shader_id; + } + + ShaderRequest(const std::string &shader_id, const std::string &shader_stub) + { + id = shader_id; + stub = shader_stub; + } + + std::string id; + std::string stub; + }; + + /** + * @brief Generate hardware accelerated code for this Node + */ + virtual ShaderCode get_shader_code(const ShaderRequest &request) const; + + /** + * @brief If Value() pushes a ShaderJob, this is the function that will process them. + */ + virtual void process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const; + + /** + * @brief If Value() pushes a GenerateJob, override this function for the image to create + * + * @param frame + * + * The destination buffer. It will already be allocated and ready for writing to. + */ + virtual void generate_frame(FramePtr frame, const GenerateJob &job) const; + + /** + * @brief Returns whether this node ever receives an input from a particular node instance + */ + bool inputs_from(Node *n, bool recursively) const; + + /** + * @brief Returns whether this node ever receives an input from a node with a particular ID + */ + bool inputs_from(const std::string &id, bool recursively) const; + + /** + * @brief Find inputs that `output` outputs to in order to arrive at this node + * + * Traverse this node's inputs recursively looking for `output`, and return a list of + * edges that `output` uses to get to `this` node. + */ + std::vector find_ways_node_arrives_here(const Node *output) const; + + /** + * @brief Severs all input and output connections + */ + void disconnect_all(); + + /** + * @brief Get the human-readable name for any category + */ + static std::string get_category_name(const CategoryID &c); + + enum TransformTimeDirection { + k_towards_input, + k_towards_output + }; + + /** + * @brief Transforms time from this node through the connections it takes to get to the specified node + */ + TimeRange transform_time_to(TimeRange time, Node *target, + TransformTimeDirection dir, int path_index); + + /** + * @brief Find nodes of a certain type that this Node takes inputs from + */ + template std::vector find_input_nodes(int maximum = 0) const; + + /** + * @brief Find nodes of a certain type that this Node takes inputs from + */ + template + static std::vector find_input_nodes_connected_to_input(const NodeInput &input, + int maximum = 0); + + using InvalidateCacheOptions = std::map; + + /** + * @brief Signal all dependent Nodes that anything cached between start_range and end_range is now invalid and + * requires re-rendering + * + * Override this if your Node subclass keeps a cache, but call this base function at the end of the subclass function. + * Default behavior is to relay this signal to all connected outputs, which will need to be done as to not break + * the DAG. Even if the time needs to be transformed somehow (e.g. converting media time to sequence time), you can + * call this function with transformed time and relay the signal that way. + */ + virtual void + invalidate_cache(const TimeRange &range, const std::string &from, + int element = -1, + InvalidateCacheOptions options = InvalidateCacheOptions()); + + void invalidate_cache( + const TimeRange &range, const NodeInput &from, + const InvalidateCacheOptions &options = InvalidateCacheOptions()) + { + invalidate_cache(range, from.input(), from.element(), options); + } + + /** + * @brief Adjusts time that should be sent to nodes connected to certain inputs. + * + * If this node modifies the `time` (i.e. a clip converting sequence time to media time), this function should be + * overridden to do so. Also make sure to override OutputTimeAdjustment() to provide the inverse function. + */ + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const; + + /** + * @brief The inverse of InputTimeAdjustment() + */ + virtual TimeRange output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const; + + /** + * @brief Copies inputs from from Node to another including connections + * + * Nodes must be of the same types (i.e. have the same ID) + */ + static void copy_inputs(const Node *source, Node *destination, + bool include_connections = true, + MultiUndoCommand *command = nullptr); + + static void copy_input(const Node *src, Node *dst, const std::string &input, + bool include_connections, bool traverse_arrays, + MultiUndoCommand *command); + + static void copy_values_of_element(const Node *src, Node *dst, + const std::string &input, int src_element, + int dst_element, + MultiUndoCommand *command = nullptr); + static void copy_values_of_element(const Node *src, Node *dst, + const std::string &input, int element, + MultiUndoCommand *command = nullptr) + { + return copy_values_of_element(src, dst, input, element, element, command); + } + + /** + * @brief Clones a set of nodes and connects the new ones the way the old ones were + */ + static std::vector copy_dependency_graph(const std::vector &nodes, + MultiUndoCommand *command); + static void copy_dependency_graph(const std::vector &src, + const std::vector &dst, + MultiUndoCommand *command); + + static Node * + copy_node_and_dependency_graph_minus_items(Node *node, MultiUndoCommand *command); + + static Node *copy_node_in_graph(Node *node, MultiUndoCommand *command); + + /** + * @brief The main processing function + * + * The node's main purpose is to take values from inputs to set values in outputs. For whatever subclass node you + * create, this is where the code for that goes. + * + * Note that as a video editor, the node graph has to work across time. Depending on the purpose of your node, it may + * output different values depending on the time, and even if not, it will likely be receiving different input + * depending on the time. Most of the difficult work here is handled by NodeInput::get_value() which you should pass + * the `time` parameter to. It will return its value (at that time, if it's keyframed), or pass the time to a + * corresponding output if it's connected to one. If your node doesn't directly deal with time, the default behavior + * of the NodeParam objects will handle everything related to it automatically. + */ + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const; + + bool has_gizmos() const + { + return !gizmos_.empty(); + } + + const std::vector &get_gizmos() const + { + return gizmos_; + } + + virtual Matrix4x4 gizmo_transformation(const NodeValueRow &row, + const NodeGlobals &globals) const + { + return Matrix4x4(); + } + + virtual void update_gizmo_positions(const NodeValueRow &row, + const NodeGlobals &globals) + { + } + + const std::string &get_label() const; + void set_label(const std::string &s); + + std::string get_label_and_name() const; + std::string get_label_or_name() const; + + void invalidate_all(const std::string &input, int element = -1); + + bool has_links() const + { + return !links_.empty(); + } + + const std::vector &links() const + { + return links_; + } + + static bool link(Node *a, Node *b); + static bool unlink(Node *a, Node *b); + static bool are_linked(Node *a, Node *b); + + bool load(XmlStreamReader *reader, SerializedData *data); + void save(XmlStreamWriter *writer) const; + + virtual bool load_custom(XmlStreamReader *reader, SerializedData *data); + virtual void save_custom(XmlStreamWriter *writer) const + { + } + virtual void PostLoadEvent(SerializedData *data); + + bool load_input(XmlStreamReader *reader, SerializedData *data); + void save_input(XmlStreamWriter *writer, const std::string &id) const; + + /** + * @brief Maps an input ID read from an old project file to its current ID + * + * Nodes whose input IDs have been renamed override this so old projects + * keep loading. The default implementation returns the ID unchanged. + */ + virtual std::string get_input_id_for_legacy_id(const std::string &id) const; + + bool load_immediate(XmlStreamReader *reader, const std::string &input, + int element, SerializedData *data); + void save_immediate(XmlStreamWriter *writer, const std::string &input, + int element) const; + + void set_folder(Folder *folder) + { + folder_ = folder; + } + + InputFlags get_input_flags(const std::string &input) const; + void set_input_flag(const std::string &input, InputFlag f, bool on = true); + + virtual void LoadFinishedEvent() + { + } + virtual void ConnectedToPreviewEvent() + { + } + + static void set_value_at_time(const NodeInput &input, const Rational &time, + const Variant &value, int track, + MultiUndoCommand *command, + bool insert_on_all_tracks_if_no_key); + + /** + * @brief Find path starting at `from` that outputs to arrive at `to` + */ + static std::list find_path(Node *from, Node *to, int path_index); + + void array_resize_internal(const std::string &id, int size); + + virtual void AddedToGraphEvent(Project *p) + { + } + virtual void RemovedFromGraphEvent(Project *p) + { + } + + static std::string get_connect_command_string(Node *output, + const NodeInput &input); + static std::string get_disconnect_command_string(Node *output, + const NodeInput &input); + + static const std::string k_enabled_input; + + /** + * @brief Registers a keyframe with this node + * + * Replaces the QObject childAdded path: inserts the keyframe into its + * input immediate (sorted by time) and invalidates the affected range. + */ + void add_keyframe(NodeKeyframe *key); + + /** + * @brief Unregisters a keyframe from this node + * + * Replaces the QObject childRemoved path: removes the keyframe from its + * input immediate and invalidates the range it affected. + */ + void remove_keyframe(NodeKeyframe *key); + + /** + * @brief Registers a gizmo with this node (replaces QObject childAdded) + */ + void add_gizmo(NodeGizmo *gizmo) + { + gizmos_.push_back(gizmo); + } + + /** + * @brief Unregisters a gizmo from this node (replaces QObject childRemoved) + */ + void remove_gizmo(NodeGizmo *gizmo) + { + auto it = std::find(gizmos_.begin(), gizmos_.end(), gizmo); + if (it != gizmos_.end()) { + gizmos_.erase(it); + } + } + + /** + * @brief Keyframe change notifications (formerly private slots driven by + * NodeKeyframe signals and sender()) + * + * With the signal/slot mechanism removed, NodeKeyframe calls these + * directly through its node pointer. + */ + void invalidate_from_keyframe_time_change(NodeKeyframe *key); + void invalidate_from_keyframe_value_change(NodeKeyframe *key); + void invalidate_from_keyframe_type_changed(NodeKeyframe *key); + void invalidate_from_keyframe_bezier_in_change(NodeKeyframe *key); + void invalidate_from_keyframe_bezier_out_change(NodeKeyframe *key); + + OFX::Host::ImageEffect::Instance *getPluginInstance() const + { + return plugin_instance_; + } + OFX::Host::ImageEffect::ImageEffectPlugin *getPlugin() const + { + return plugin_instance_ ? plugin_instance_->getPlugin() : nullptr; + } + +protected: + // If set, this node owns a plugin instance. + OFX::Host::ImageEffect::Instance *plugin_instance_ = nullptr; + + void setPluginInstance(OFX::Host::ImageEffect::Instance *instance) + { + plugin_instance_ = instance; + } + + void insert_input(const std::string &id, NodeValue::Type type, + const Variant &default_value, InputFlags flags, + int index); + + void prepend_input(const std::string &id, NodeValue::Type type, + const Variant &default_value, + InputFlags flags = InputFlags(k_input_flag_normal)) + { + insert_input(id, type, default_value, flags, 0); + } + + void prepend_input(const std::string &id, NodeValue::Type type, + InputFlags flags = InputFlags(k_input_flag_normal)) + { + prepend_input(id, type, Variant(), flags); + } + + void add_input(const std::string &id, NodeValue::Type type, + const Variant &default_value, + InputFlags flags = InputFlags(k_input_flag_normal)) + { + insert_input(id, type, default_value, flags, int(input_ids_.size())); + } + + void add_input(const std::string &id, NodeValue::Type type, + InputFlags flags = InputFlags(k_input_flag_normal)) + { + add_input(id, type, Variant(), flags); + } + + void remove_input(const std::string &id); + + void set_combo_box_strings(const std::string &id, const StringList &strings) + { + set_input_property(id, "combo_str", strings); + } + + void send_invalidate_cache(const TimeRange &range, + const InvalidateCacheOptions &options); + + enum GizmoScaleHandles { + k_gizmo_scale_top_left, + k_gizmo_scale_top_center, + k_gizmo_scale_top_right, + k_gizmo_scale_bottom_left, + k_gizmo_scale_bottom_center, + k_gizmo_scale_bottom_right, + k_gizmo_scale_center_left, + k_gizmo_scale_center_right, + k_gizmo_scale_count, + }; + + virtual void LinkChangeEvent() + { + } + + virtual void InputValueChangedEvent(const std::string &input, int element); + + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output); + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output); + + virtual void OutputConnectedEvent(const NodeInput &input); + + virtual void OutputDisconnectedEvent(const NodeInput &input); + + void set_effect_input(const std::string &input) + { + effect_input_ = input; + } + + void set_flag(Flag f, bool on = true) + { + if (on) { + flags_ |= f; + } else { + flags_ &= ~f; + } + } + + template + T *add_draggable_gizmo(const std::vector &inputs = + std::vector(), + DraggableGizmo::DragValueBehavior behavior = + DraggableGizmo::k_delta_from_start) + { + T *gizmo = new T(this); + gizmo->set_drag_value_behavior(behavior); + for (const NodeKeyframeTrackReference &input : inputs) { + gizmo->add_input(input); + } + add_gizmo(gizmo); + return gizmo; + } + + template + T *add_draggable_gizmo(const StringList &inputs, + DraggableGizmo::DragValueBehavior behavior = + DraggableGizmo::k_delta_from_start) + { + std::vector refs(inputs.size()); + for (size_t i = 0; i < refs.size(); i++) { + refs[i] = NodeInput(this, inputs[i]); + } + return add_draggable_gizmo(refs, behavior); + } + + /** + * @brief Gizmo drag callbacks (formerly protected slots connected to + * DraggableGizmo signals) + * + * Plain virtual functions now; DraggableGizmo invokes them directly. + * `modifiers` carries the keyboard modifier flags as an int (formerly + * Qt::KeyboardModifiers). + */ + friend class DraggableGizmo; + + virtual void gizmo_drag_start(const olive::NodeValueRow &row, double x, + double y, const olive::core::Rational &time) + { + } + + virtual void gizmo_drag_move(double x, double y, int modifiers) + { + } + + /** + * @brief Gizmo currently issuing a drag callback (replaces + * QObject::sender() in gizmo_drag_start()/gizmo_drag_move()) + * + * DraggableGizmo sets this around its direct invocation of the drag + * callbacks. Null outside of a drag callback. + */ + NodeGizmo *current_gizmo() const + { + return current_gizmo_; + } + + void set_current_gizmo(NodeGizmo *gizmo) + { + current_gizmo_ = gizmo; + } + +private: + struct Input { + NodeValue::Type type; + InputFlags flags; + SplitValue default_value; + std::map properties; + std::string human_name; + int array_size; + }; + + NodeInputImmediate *create_immediate(const std::string &input); + + int get_internal_input_index(const std::string &input) const + { + auto it = std::find(input_ids_.begin(), input_ids_.end(), input); + if (it == input_ids_.end()) { + return -1; + } else { + return int(it - input_ids_.begin()); + } + } + + Input *get_internal_input_data(const std::string &input) + { + int i = get_internal_input_index(input); + + if (i == -1) { + return nullptr; + } else { + return &input_data_[i]; + } + } + + const Input *get_internal_input_data(const std::string &input) const + { + int i = get_internal_input_index(input); + + if (i == -1) { + return nullptr; + } else { + return &input_data_.at(i); + } + } + + void report_invalid_input(const char *attempted_action, const std::string &id, + int element) const; + + static Node *copy_node_and_dependency_graph_minus_items_internal( + std::map &created, Node *node, MultiUndoCommand *command); + + /** + * @brief Immediates aren't deleted, so the actual array size may be larger than ArraySize() + */ + int get_internal_input_array_size(const std::string &input); + + /** + * @brief Find nodes of a certain type that this Node takes inputs from + */ + template + static void find_input_nodes_connected_to_input_internal(const NodeInput &input, + std::vector &list, + int maximum); + + template + static void find_input_node_internal(const Node *n, std::vector &list, + int maximum); + + std::vector get_dependencies_internal(bool traverse, + bool exclusive_only) const; + + void parameter_value_changed(const std::string &input, int element, + const olive::core::TimeRange &range); + void parameter_value_changed(const NodeInput &input, + const olive::core::TimeRange &range) + { + parameter_value_changed(input.input(), input.element(), range); + } + + /** + * @brief Intelligently determine how what time range is affected by a keyframe + */ + TimeRange get_range_affected_by_keyframe(NodeKeyframe *key) const; + + /** + * @brief Gets a time range between the previous and next keyframes of index + */ + TimeRange get_range_around_index(const std::string &input, int index, int track, + int element) const; + + void clear_element(const std::string &input, int index); + + /** + * @brief Custom user label for node + */ + std::string label_; + + /** + * @brief -1 if the color should be based on the category, >=0 if the user has set a custom color + */ + int override_color_; + + /** + * @brief Nodes that are linked with this one + */ + std::vector links_; + + std::vector input_ids_; + std::vector input_data_; + + std::map standard_immediates_; + + std::map> array_immediates_; + + InputConnections input_connections_; + + OutputConnections output_connections_; + + Folder *folder_; + + /** + * @brief Graph this node belongs to (replaces QObject::parent()) + */ + Project *parent_; + + std::map value_hints_; + + PositionMap context_positions_; + + uint64_t flags_; + + std::vector gizmos_; + + NodeGizmo *current_gizmo_ = nullptr; + + std::string effect_input_; + + FrameHashCache *video_cache_; + ThumbnailCache *thumbnail_cache_; + + AudioPlaybackCache *audio_cache_; + AudioWaveformCache *waveform_cache_; + + bool caches_enabled_; +}; + +template +void Node::find_input_nodes_connected_to_input_internal(const NodeInput &input, + std::vector &list, + int maximum) +{ + Node *edge = input.get_connected_output(); + if (!edge) { + return; + } + + T *cast_test = dynamic_cast(edge); + + if (cast_test) { + list.push_back(cast_test); + if (maximum != 0 && int(list.size()) == maximum) { + return; + } + } + + find_input_node_internal(edge, list, maximum); +} + +template +std::vector Node::find_input_nodes_connected_to_input(const NodeInput &input, + int maximum) +{ + std::vector list; + + find_input_nodes_connected_to_input_internal(input, list, maximum); + + return list; +} + +template +void Node::find_input_node_internal(const Node *n, std::vector &list, int maximum) +{ + for (auto it = n->input_connections_.cbegin(); + it != n->input_connections_.cend(); it++) { + find_input_nodes_connected_to_input_internal(it->first, list, maximum); + } +} + +template std::vector Node::find_input_nodes(int maximum) const +{ + std::vector list; + + find_input_node_internal(this, list, maximum); + + return list; +} + +} + +#endif // OAK_NODE_H diff --git a/src/node/src/nodeundo.cpp b/src/node/src/nodeundo.cpp new file mode 100644 index 000000000..3b65bef24 --- /dev/null +++ b/src/node/src/nodeundo.cpp @@ -0,0 +1,586 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodeundo.h" + +#include +#include + +namespace olive +{ + +void NodeSetPositionCommand::redo() +{ + added_ = !context_->context_contains_node(node_); + + if (!added_) { + old_pos_ = context_->get_node_position_data_in_context(node_); + } + + context_->set_node_position_in_context(node_, pos_); +} + +void NodeSetPositionCommand::undo() +{ + if (added_) { + context_->remove_node_from_context(node_); + } else { + context_->set_node_position_in_context(node_, old_pos_); + } +} + +void NodeRemovePositionFromContextCommand::redo() +{ + contained_ = context_->context_contains_node(node_); + + if (contained_) { + old_pos_ = context_->get_node_position_data_in_context(node_); + context_->remove_node_from_context(node_); + } +} + +void NodeRemovePositionFromContextCommand::undo() +{ + if (contained_) { + context_->set_node_position_in_context(node_, old_pos_); + } +} + +void NodeRemovePositionFromAllContextsCommand::redo() +{ + Project *graph = node_->parent(); + + for (Node *context : graph->nodes()) { + if (context->context_contains_node(node_)) { + contexts_.insert( + { context, context->get_node_position_in_context(node_) }); + context->remove_node_from_context(node_); + } + } +} + +void NodeRemovePositionFromAllContextsCommand::undo() +{ + for (auto it = contexts_.crbegin(); it != contexts_.crend(); it++) { + it->first->set_node_position_in_context(node_, it->second); + } + + contexts_.clear(); +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::prepare() +{ + move_recursively( + node_, + pos_.position - context_->get_node_position_data_in_context(node_).position); +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::redo() +{ + for (auto it = commands_.cbegin(); it != commands_.cend(); it++) { + (*it)->redo_now(); + } +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::undo() +{ + for (auto it = commands_.crbegin(); it != commands_.crend(); it++) { + (*it)->undo_now(); + } +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::move_recursively( + Node *node, const PointF &diff) +{ + Node::Position pos = context_->get_node_position_data_in_context(node); + pos += diff; + commands_.push_back(new NodeSetPositionCommand(node, context_, pos)); + + for (auto it = node->input_connections().cbegin(); + it != node->input_connections().cend(); it++) { + Node *output = it->second; + if (context_->context_contains_node(output)) { + move_recursively(output, diff); + } + } +} + +NodeEdgeAddCommand::NodeEdgeAddCommand(Node *output, const NodeInput &input) + : output_(output) + , input_(input) + , remove_command_(nullptr) +{ +} + +NodeEdgeAddCommand::~NodeEdgeAddCommand() +{ + delete remove_command_; +} + +void NodeEdgeAddCommand::redo() +{ + if (std::getenv("OAK_DEBUG_EDGES")) { + fprintf(stderr, "EDGE-DEBUG: NodeEdgeAddCommand::redo %p -> %p (%s)\n", + (void *)output_, (void *)input_.node(), input_.input().c_str()); + } + + if (input_.is_connected()) { + if (!remove_command_) { + remove_command_ = + new NodeEdgeRemoveCommand(input_.get_connected_output(), input_); + } + + remove_command_->redo_now(); + } + + Node::connect_edge(output_, input_); +} + +void NodeEdgeAddCommand::undo() +{ + if (std::getenv("OAK_DEBUG_EDGES")) { + fprintf(stderr, "EDGE-DEBUG: NodeEdgeAddCommand::undo %p -> %p (%s)\n", + (void *)output_, (void *)input_.node(), input_.input().c_str()); + } + + Node::disconnect_edge(output_, input_); + + if (remove_command_) { + remove_command_->undo_now(); + } +} + +NodeEdgeRemoveCommand::NodeEdgeRemoveCommand(Node *output, + const NodeInput &input) + : output_(output) + , input_(input) +{ +} + +void NodeEdgeRemoveCommand::redo() +{ + if (std::getenv("OAK_DEBUG_EDGES")) { + fprintf(stderr, "EDGE-DEBUG: NodeEdgeRemoveCommand::redo %p -> %p (%s)\n", + (void *)output_, (void *)input_.node(), input_.input().c_str()); + } + + Node::disconnect_edge(output_, input_); +} + +void NodeEdgeRemoveCommand::undo() +{ + if (std::getenv("OAK_DEBUG_EDGES")) { + fprintf(stderr, "EDGE-DEBUG: NodeEdgeRemoveCommand::undo %p -> %p (%s)\n", + (void *)output_, (void *)input_.node(), input_.input().c_str()); + } + + Node::connect_edge(output_, input_); +} + +NodeAddCommand::NodeAddCommand(Project *graph, Node *node) + : graph_(graph) + , node_(node) +{ + // Ensures that when this command is destroyed, if redo() is never called again, the node will be destroyed too + if (Project *old_graph = node_->parent()) { + old_graph->remove_node(node_); + } + owned_node_.reset(node_); +} + +void NodeAddCommand::redo() +{ + graph_->add_node(owned_node_.release()); +} + +void NodeAddCommand::undo() +{ + graph_->remove_node(node_); + owned_node_.reset(node_); +} + +void NodeRemoveAndDisconnectCommand::prepare() +{ + command_ = new MultiUndoCommand(); + + // If this is a block, remove all links + if (node_->has_links()) { + command_->add_child(new NodeUnlinkAllCommand(node_)); + } + + // Disconnect everything + for (auto it = node_->input_connections().cbegin(); + it != node_->input_connections().cend(); it++) { + command_->add_child(new NodeEdgeRemoveCommand(it->second, it->first)); + } + + for (const Node::OutputConnection &conn : node_->output_connections()) { + command_->add_child(new NodeEdgeRemoveCommand(conn.first, conn.second)); + } + + command_->add_child(new NodeRemovePositionFromAllContextsCommand(node_)); +} + +void NodeRenameCommand::add_node(Node *node, const std::string &new_name) +{ + nodes_.push_back(node); + new_labels_.push_back(new_name); + old_labels_.push_back(node->get_label()); +} + +void NodeRenameCommand::redo() +{ + for (size_t i = 0; i < nodes_.size(); i++) { + nodes_.at(i)->set_label(new_labels_.at(i)); + } +} + +void NodeRenameCommand::undo() +{ + for (size_t i = 0; i < nodes_.size(); i++) { + nodes_.at(i)->set_label(old_labels_.at(i)); + } +} + +NodeOverrideColorCommand::NodeOverrideColorCommand(Node *node, int index) + : node_(node) + , new_index_(index) +{ +} + +void NodeOverrideColorCommand::redo() +{ + old_index_ = node_->get_override_color(); + node_->set_override_color(new_index_); +} + +void NodeOverrideColorCommand::undo() +{ + node_->set_override_color(old_index_); +} + +NodeViewDeleteCommand::NodeViewDeleteCommand() +{ +} + +void NodeViewDeleteCommand::add_node(Node *node, Node *context) +{ + if (contains_node(node, context)) { + return; + } + + Node::ContextPair p = { node, context }; + nodes_.push_back(p); + + for (auto it = node->input_connections().cbegin(); + it != node->input_connections().cend(); it++) { + if (context->context_contains_node(it->second)) { + add_edge(it->second, it->first); + } + } + + for (auto it = node->output_connections().cbegin(); + it != node->output_connections().cend(); it++) { + if (context->context_contains_node(it->second.node())) { + add_edge(it->first, it->second); + } + } +} + +void NodeViewDeleteCommand::add_edge(Node *output, const NodeInput &input) +{ + for (const Node::OutputConnection &edge : edges_) { + if (edge.first == output && edge.second == input) { + return; + } + } + + edges_.push_back({ output, input }); +} + +bool NodeViewDeleteCommand::contains_node(Node *node, Node *context) +{ + for (const Node::ContextPair &pair : nodes_) { + if (pair.node == node && pair.context == context) { + return true; + } + } + + return false; +} + +void NodeViewDeleteCommand::redo() +{ + for (const Node::OutputConnection &edge : edges_) { + Node::disconnect_edge(edge.first, edge.second); + } + + for (const Node::ContextPair &pair : nodes_) { + RemovedNode rn; + + rn.node = pair.node; + rn.context = pair.context; + rn.pos = rn.context->get_node_position_in_context(rn.node); + + rn.context->remove_node_from_context(rn.node); + + // If node is no longer in any contexts and is not connected to anything, remove it + if (rn.node->parent()->get_number_of_contexts_node_is_in(rn.node, true) == + 0 && + rn.node->input_connections().empty() && + rn.node->output_connections().empty()) { + rn.removed_from_graph = rn.node->parent(); + rn.removed_from_graph->remove_node(rn.node); + owned_nodes_.emplace_back(rn.node); + } else { + rn.removed_from_graph = nullptr; + } + + removed_nodes_.push_back(rn); + } +} + +void NodeViewDeleteCommand::undo() +{ + for (auto rn = removed_nodes_.crbegin(); rn != removed_nodes_.crend(); + rn++) { + if (rn->removed_from_graph) { + rn->removed_from_graph->add_node(rn->node); + } + + rn->context->set_node_position_in_context(rn->node, rn->pos); + } + removed_nodes_.clear(); + + for (auto &node : owned_nodes_) { + node.release(); + } + owned_nodes_.clear(); + + for (auto edge = edges_.crbegin(); edge != edges_.crend(); edge++) { + Node::connect_edge(edge->first, edge->second); + } +} + +NodeParamSetKeyframingCommand::NodeParamSetKeyframingCommand( + const NodeInput &input, bool setting) + : input_(input) + , new_setting_(setting) +{ +} + +void NodeParamSetKeyframingCommand::redo() +{ + old_setting_ = input_.is_keyframing(); + input_.node()->set_input_is_keyframing(input_, new_setting_); +} + +void NodeParamSetKeyframingCommand::undo() +{ + input_.node()->set_input_is_keyframing(input_, old_setting_); +} + +NodeParamSetKeyframeValueCommand::NodeParamSetKeyframeValueCommand( + NodeKeyframe *key, const Variant &value) + : key_(key) + , old_value_(key_->value()) + , new_value_(value) +{ +} + +NodeParamSetKeyframeValueCommand::NodeParamSetKeyframeValueCommand( + NodeKeyframe *key, const Variant &new_value, const Variant &old_value) + : key_(key) + , old_value_(old_value) + , new_value_(new_value) +{ +} + +void NodeParamSetKeyframeValueCommand::redo() +{ + key_->set_value(new_value_); +} + +void NodeParamSetKeyframeValueCommand::undo() +{ + key_->set_value(old_value_); +} + +NodeParamInsertKeyframeCommand::NodeParamInsertKeyframeCommand( + Node *node, NodeKeyframe *keyframe) + : input_(node) + , keyframe_(keyframe) +{ + // Take ownership of the keyframe + undo(); +} + +void NodeParamInsertKeyframeCommand::redo() +{ + input_->add_keyframe(keyframe_); + owned_keyframe_.release(); +} + +void NodeParamInsertKeyframeCommand::undo() +{ + if (Node *parent = keyframe_->parent()) { + parent->remove_keyframe(keyframe_); + } + owned_keyframe_.reset(keyframe_); +} + +NodeParamRemoveKeyframeCommand::NodeParamRemoveKeyframeCommand( + NodeKeyframe *keyframe) + : input_(keyframe->parent()) + , keyframe_(keyframe) +{ +} + +void NodeParamRemoveKeyframeCommand::redo() +{ + // Removes from input + if (Node *parent = keyframe_->parent()) { + parent->remove_keyframe(keyframe_); + } + owned_keyframe_.reset(keyframe_); +} + +void NodeParamRemoveKeyframeCommand::undo() +{ + input_->add_keyframe(keyframe_); + owned_keyframe_.release(); +} + +NodeParamSetKeyframeTimeCommand::NodeParamSetKeyframeTimeCommand( + NodeKeyframe *key, const Rational &time) + : key_(key) + , old_time_(key->time()) + , new_time_(time) +{ +} + +NodeParamSetKeyframeTimeCommand::NodeParamSetKeyframeTimeCommand( + NodeKeyframe *key, const Rational &new_time, const Rational &old_time) + : key_(key) + , old_time_(old_time) + , new_time_(new_time) +{ +} + +void NodeParamSetKeyframeTimeCommand::redo() +{ + key_->set_time(new_time_); +} + +void NodeParamSetKeyframeTimeCommand::undo() +{ + key_->set_time(old_time_); +} + +NodeParamSetStandardValueCommand::NodeParamSetStandardValueCommand( + const NodeKeyframeTrackReference &input, const Variant &value) + : ref_(input) + , old_value_(ref_.input().node()->get_standard_value(ref_.input())) + , new_value_(value) +{ +} + +NodeParamSetStandardValueCommand::NodeParamSetStandardValueCommand( + const NodeKeyframeTrackReference &input, const Variant &new_value, + const Variant &old_value) + : ref_(input) + , old_value_(old_value) + , new_value_(new_value) +{ +} + +void NodeParamSetStandardValueCommand::redo() +{ + ref_.input().node()->set_split_standard_value_on_track(ref_, new_value_); +} + +void NodeParamSetStandardValueCommand::undo() +{ + ref_.input().node()->set_split_standard_value_on_track(ref_, old_value_); +} + +NodeParamArrayAppendCommand::NodeParamArrayAppendCommand(Node *node, + const std::string &input) + : node_(node) + , input_(input) +{ +} + +void NodeParamArrayAppendCommand::redo() +{ + node_->input_array_append(input_); +} + +void NodeParamArrayAppendCommand::undo() +{ + node_->input_array_remove_last(input_); +} + +void NodeSetValueHintCommand::redo() +{ + old_hint_ = + input_.node()->get_value_hint_for_input(input_.input(), input_.element()); + input_.node()->set_value_hint_for_input(input_.input(), new_hint_, + input_.element()); +} + +void NodeSetValueHintCommand::undo() +{ + input_.node()->set_value_hint_for_input(input_.input(), old_hint_, + input_.element()); +} + +void NodeImmediateRemoveAllKeyframesCommand::prepare() +{ + for (const NodeKeyframeTrack &track : immediate_->keyframe_tracks()) { + keys_.insert(keys_.end(), track.begin(), track.end()); + } + + if (!keys_.empty()) { + node_ = keys_.front()->parent(); + } +} + +void NodeImmediateRemoveAllKeyframesCommand::redo() +{ + for (auto it = keys_.cbegin(); it != keys_.cend(); it++) { + node_->remove_keyframe(*it); + owned_keyframes_.emplace_back(*it); + } +} + +void NodeImmediateRemoveAllKeyframesCommand::undo() +{ + for (auto it = keys_.crbegin(); it != keys_.crend(); it++) { + node_->add_keyframe(*it); + } + for (auto &key : owned_keyframes_) { + key.release(); + } + owned_keyframes_.clear(); +} + +} diff --git a/src/node/src/nodeundo.h b/src/node/src/nodeundo.h new file mode 100644 index 000000000..8e835db93 --- /dev/null +++ b/src/node/src/nodeundo.h @@ -0,0 +1,770 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEUNDO_H +#define OAK_NODEUNDO_H + +#include +#include +#include +#include + +#include "node.h" +#include "project.h" +#include "undocommand.h" + +namespace olive +{ + +class NodeSetPositionCommand : public UndoCommand { +public: + NodeSetPositionCommand(Node *node, Node *context, const Node::Position &pos) + { + node_ = node; + context_ = context; + pos_ = pos; + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + Node *context_; + Node::Position pos_; + Node::Position old_pos_; + bool added_; +}; + +class NodeSetPositionAndDependenciesRecursivelyCommand : public UndoCommand { +public: + NodeSetPositionAndDependenciesRecursivelyCommand(Node *node, Node *context, + const Node::Position &pos) + : node_(node) + , context_(context) + , pos_(pos) + { + } + +protected: + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + void move_recursively(Node *node, const PointF &diff); + + Node *node_; + Node *context_; + Node::Position pos_; + std::vector commands_; +}; + +class NodeRemovePositionFromContextCommand : public UndoCommand { +public: + NodeRemovePositionFromContextCommand(Node *node, Node *context) + : node_(node) + , context_(context) + { + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + Node *context_; + + Node::Position old_pos_; + + bool contained_; +}; + +class NodeRemovePositionFromAllContextsCommand : public UndoCommand { +public: + NodeRemovePositionFromAllContextsCommand(Node *node) + : node_(node) + { + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + std::map contexts_; +}; + +class NodeArrayInsertCommand : public UndoCommand { +public: + NodeArrayInsertCommand(Node *node, const std::string &input, int index) + : node_(node) + , input_(input) + , index_(index) + { + } + +protected: + virtual void redo() override + { + node_->input_array_insert(input_, index_); + } + + virtual void undo() override + { + node_->input_array_remove(input_, index_); + } + +private: + Node *node_; + std::string input_; + int index_; +}; + +class NodeArrayResizeCommand : public UndoCommand { +public: + NodeArrayResizeCommand(Node *node, const std::string &input, int size) + : node_(node) + , input_(input) + , size_(size) + { + } + +protected: + virtual void redo() override + { + old_size_ = node_->input_array_size(input_); + + if (old_size_ > size_) { + // Decreasing in size, disconnect any extraneous edges + for (int i = size_; i < old_size_; i++) { + try { + NodeInput input(node_, input_, i); + Node *output = node_->input_connections().at(input); + + removed_connections_[input] = output; + + Node::disconnect_edge(output, input); + } catch (std::out_of_range &) { + } + } + } + + node_->array_resize_internal(input_, size_); + } + + virtual void undo() override + { + for (auto it = removed_connections_.cbegin(); + it != removed_connections_.cend(); it++) { + Node::connect_edge(it->second, it->first); + } + removed_connections_.clear(); + + node_->array_resize_internal(input_, old_size_); + } + +private: + Node *node_; + std::string input_; + int size_; + int old_size_; + + Node::InputConnections removed_connections_; +}; + +class NodeArrayRemoveCommand : public UndoCommand { +public: + NodeArrayRemoveCommand(Node *node, const std::string &input, int index) + : node_(node) + , input_(input) + , index_(index) + { + } + +protected: + virtual void redo() override + { + // Save immediate data + if (node_->is_input_keyframable(input_)) { + is_keyframing_ = node_->is_input_keyframing(input_, index_); + } + standard_value_ = node_->get_split_standard_value(input_, index_); + keyframes_ = node_->get_keyframe_tracks(input_, index_); + + // Take ownership of the keyframes (removes them from the immediate) + std::vector reclaimed; + node_->get_immediate(input_, index_)->delete_all_keyframes(&reclaimed); + for (NodeKeyframe *key : reclaimed) { + owned_keyframes_.emplace_back(key); + } + + node_->input_array_remove(input_, index_); + } + + virtual void undo() override + { + node_->input_array_insert(input_, index_); + + // Restore keyframes + for (const NodeKeyframeTrack &track : keyframes_) { + for (NodeKeyframe *key : track) { + node_->add_keyframe(key); + } + } + for (auto &key : owned_keyframes_) { + key.release(); + } + owned_keyframes_.clear(); + + node_->set_split_standard_value(input_, standard_value_, index_); + + if (node_->is_input_keyframable(input_)) { + node_->set_input_is_keyframing(input_, is_keyframing_, index_); + } + } + +private: + Node *node_; + std::string input_; + int index_; + + SplitValue standard_value_; + bool is_keyframing_; + std::vector keyframes_; + std::vector> owned_keyframes_; +}; + +/** + * @brief An undoable command for disconnecting two NodeParams + * + * Can be considered a UndoCommand wrapper for NodeParam::DisonnectEdge()/ + */ +class NodeEdgeRemoveCommand : public UndoCommand { +public: + NodeEdgeRemoveCommand(Node *output, const NodeInput &input); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + Node *output_; + NodeInput input_; +}; + +/** + * @brief An undoable command for connecting two NodeParams together + * + * Can be considered a UndoCommand wrapper for NodeParam::ConnectEdge()/ + */ +class NodeEdgeAddCommand : public UndoCommand { +public: + NodeEdgeAddCommand(Node *output, const NodeInput &input); + + virtual ~NodeEdgeAddCommand() override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + Node *output_; + NodeInput input_; + + NodeEdgeRemoveCommand *remove_command_; +}; + +class NodeAddCommand : public UndoCommand { +public: + NodeAddCommand(Project *graph, Node *node); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + std::unique_ptr owned_node_; + + Project *graph_; + Node *node_; +}; + +class NodeRemoveAndDisconnectCommand : public UndoCommand { +public: + NodeRemoveAndDisconnectCommand(Node *node) + : node_(node) + , graph_(nullptr) + , command_(nullptr) + { + } + + virtual ~NodeRemoveAndDisconnectCommand() override + { + delete command_; + } + +protected: + virtual void prepare() override; + + virtual void redo() override + { + command_->redo_now(); + + graph_ = node_->parent(); + graph_->remove_node(node_); + owned_node_.reset(node_); + } + + virtual void undo() override + { + graph_->add_node(owned_node_.release()); + graph_ = nullptr; + + command_->undo_now(); + } + +private: + std::unique_ptr owned_node_; + + Node *node_; + Project *graph_; + + MultiUndoCommand *command_; +}; + +class NodeRemoveWithExclusiveDependenciesAndDisconnect : public UndoCommand { +public: + NodeRemoveWithExclusiveDependenciesAndDisconnect(Node *node) + : node_(node) + , command_(nullptr) + { + } + + virtual ~NodeRemoveWithExclusiveDependenciesAndDisconnect() override + { + delete command_; + } + +protected: + virtual void prepare() override + { + command_ = new MultiUndoCommand(); + + command_->add_child(new NodeRemoveAndDisconnectCommand(node_)); + + // Remove exclusive dependencies + std::vector deps = node_->get_exclusive_dependencies(); + for (Node *d : deps) { + command_->add_child(new NodeRemoveAndDisconnectCommand(d)); + } + } + + virtual void redo() override + { + command_->redo_now(); + } + + virtual void undo() override + { + command_->undo_now(); + } + +private: + Node *node_; + MultiUndoCommand *command_; +}; + +class NodeLinkCommand : public UndoCommand { +public: + NodeLinkCommand(Node *a, Node *b, bool link) + : a_(a) + , b_(b) + , link_(link) + { + } + +protected: + virtual void redo() override + { + if (link_) { + done_ = Node::link(a_, b_); + } else { + done_ = Node::unlink(a_, b_); + } + } + + virtual void undo() override + { + if (done_) { + if (link_) { + Node::unlink(a_, b_); + } else { + Node::link(a_, b_); + } + } + } + +private: + Node *a_; + Node *b_; + bool link_; + bool done_; +}; + +class NodeUnlinkAllCommand : public UndoCommand { +public: + NodeUnlinkAllCommand(Node *node) + : node_(node) + { + } + +protected: + virtual void redo() override + { + unlinked_ = node_->links(); + + for (Node *link : unlinked_) { + Node::unlink(node_, link); + } + } + + virtual void undo() override + { + for (Node *link : unlinked_) { + Node::link(node_, link); + } + + unlinked_.clear(); + } + +private: + Node *node_; + + std::vector unlinked_; +}; + +class NodeLinkManyCommand : public MultiUndoCommand { +public: + NodeLinkManyCommand(const std::vector nodes, bool link) + : nodes_(nodes) + { + for (Node *a : nodes_) { + for (Node *b : nodes_) { + if (a != b) { + add_child(new NodeLinkCommand(a, b, link)); + } + } + } + } + +private: + std::vector nodes_; +}; + +class NodeRenameCommand : public UndoCommand { +public: + NodeRenameCommand() = default; + NodeRenameCommand(Node *node, const std::string &new_name) + { + add_node(node, new_name); + } + + void add_node(Node *node, const std::string &new_name); + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + std::vector nodes_; + + StringList new_labels_; + StringList old_labels_; +}; + +class NodeOverrideColorCommand : public UndoCommand { +public: + NodeOverrideColorCommand(Node *node, int index); + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + int old_index_; + + int new_index_; +}; + +class NodeViewDeleteCommand : public UndoCommand { +public: + NodeViewDeleteCommand(); + + void add_node(Node *node, Node *context); + + void add_edge(Node *output, const NodeInput &input); + + bool contains_node(Node *node, Node *context); + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + std::vector nodes_; + + std::vector edges_; + + struct RemovedNode { + Node *node; + Node *context; + PointF pos; + Project *removed_from_graph; + }; + + std::vector removed_nodes_; + + std::vector> owned_nodes_; +}; + +class NodeParamSetKeyframingCommand : public UndoCommand { +public: + NodeParamSetKeyframingCommand(const NodeInput &input, bool setting); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + NodeInput input_; + bool new_setting_; + bool old_setting_; +}; + +class NodeParamInsertKeyframeCommand : public UndoCommand { +public: + NodeParamInsertKeyframeCommand(Node *node, NodeKeyframe *keyframe); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + Node *input_; + + NodeKeyframe *keyframe_; + + std::unique_ptr owned_keyframe_; +}; + +class NodeParamRemoveKeyframeCommand : public UndoCommand { +public: + NodeParamRemoveKeyframeCommand(NodeKeyframe *keyframe); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + Node *input_; + + NodeKeyframe *keyframe_; + + std::unique_ptr owned_keyframe_; +}; + +class NodeParamSetKeyframeTimeCommand : public UndoCommand { +public: + NodeParamSetKeyframeTimeCommand(NodeKeyframe *key, const Rational &time); + NodeParamSetKeyframeTimeCommand(NodeKeyframe *key, const Rational &new_time, + const Rational &old_time); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + NodeKeyframe *key_; + + Rational old_time_; + Rational new_time_; +}; + +class NodeParamSetKeyframeValueCommand : public UndoCommand { +public: + NodeParamSetKeyframeValueCommand(NodeKeyframe *key, const Variant &value); + NodeParamSetKeyframeValueCommand(NodeKeyframe *key, + const Variant &new_value, + const Variant &old_value); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + NodeKeyframe *key_; + + Variant old_value_; + Variant new_value_; +}; + +class NodeParamSetStandardValueCommand : public UndoCommand { +public: + NodeParamSetStandardValueCommand(const NodeKeyframeTrackReference &input, + const Variant &value); + NodeParamSetStandardValueCommand(const NodeKeyframeTrackReference &input, + const Variant &new_value, + const Variant &old_value); + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + NodeKeyframeTrackReference ref_; + + Variant old_value_; + Variant new_value_; +}; + +class NodeParamSetSplitStandardValueCommand : public UndoCommand { +public: + NodeParamSetSplitStandardValueCommand(const NodeInput &input, + const SplitValue &new_value, + const SplitValue &old_value) + : ref_(input) + , old_value_(old_value) + , new_value_(new_value) + { + } + + NodeParamSetSplitStandardValueCommand(const NodeInput &input, + const SplitValue &value) + : NodeParamSetSplitStandardValueCommand( + input, value, input.node()->get_split_standard_value(input.input())) + { + } + +protected: + virtual void redo() override + { + ref_.node()->set_split_standard_value(ref_.input(), new_value_, + ref_.element()); + } + + virtual void undo() override + { + ref_.node()->set_split_standard_value(ref_.input(), old_value_, + ref_.element()); + } + +private: + NodeInput ref_; + + SplitValue old_value_; + SplitValue new_value_; +}; + +class NodeParamArrayAppendCommand : public UndoCommand { +public: + NodeParamArrayAppendCommand(Node *node, const std::string &input); + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + std::string input_; +}; + +class NodeSetValueHintCommand : public UndoCommand { +public: + NodeSetValueHintCommand(const NodeInput &input, const Node::ValueHint &hint) + : input_(input) + , new_hint_(hint) + { + } + + NodeSetValueHintCommand(Node *node, const std::string &input, int element, + const Node::ValueHint &hint) + : NodeSetValueHintCommand(NodeInput(node, input, element), hint) + { + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeInput input_; + + Node::ValueHint new_hint_; + Node::ValueHint old_hint_; +}; + +class NodeImmediateRemoveAllKeyframesCommand : public UndoCommand { +public: + NodeImmediateRemoveAllKeyframesCommand(NodeInputImmediate *immediate) + : immediate_(immediate) + , node_(nullptr) + { + } + +protected: + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + NodeInputImmediate *immediate_; + + Node *node_; + + std::vector> owned_keyframes_; + + std::vector keys_; +}; + +} + +#endif // OAK_NODEUNDO_H diff --git a/src/node/src/output/CMakeLists.txt b/src/node/src/output/CMakeLists.txt new file mode 100644 index 000000000..1ab4baca6 --- /dev/null +++ b/src/node/src/output/CMakeLists.txt @@ -0,0 +1,23 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(track) +add_subdirectory(viewer) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/output/track/CMakeLists.txt b/src/node/src/output/track/CMakeLists.txt new file mode 100644 index 000000000..a7ff78d00 --- /dev/null +++ b/src/node/src/output/track/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/output/track/track.cpp + node/output/track/track.h + node/output/track/tracklist.cpp + node/output/track/tracklist.h + PARENT_SCOPE +) diff --git a/src/node/src/output/track/track.cpp b/src/node/src/output/track/track.cpp new file mode 100644 index 000000000..b85bcb1ec --- /dev/null +++ b/src/node/src/output/track/track.cpp @@ -0,0 +1,880 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "track.h" + +#include +#include +#include +#include + +#include "audio/audioprocessor.h" +#include "block/clip/clip.h" +#include "block/gap/gap.h" +#include "block/transition/transition.h" + +namespace olive +{ + +#define super Node + +const double Track::k_track_height_default = 3.0; +const double Track::k_track_height_minimum = 1.5; +const double Track::k_track_height_interval = 0.5; + +const std::string Track::k_block_input = "block_in"; +const std::string Track::k_muted_input = "muted_in"; +const std::string Track::k_array_map_input = "arraymap_in"; + +static std::string number_to_string(double d) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", d); + return buf; +} + +// QVector::indexOf() equivalent: returns -1 when not found +template +static int index_of(const std::vector &list, const T &item) +{ + auto it = std::find(list.begin(), list.end(), item); + return (it == list.end()) ? -1 : int(it - list.begin()); +} + +Track::Track() + : track_type_(Track::k_none) + , index_(-1) + , locked_(false) + , sequence_(nullptr) + , ignore_arraymap_(0) + , arraymap_invalid_(false) + , ignore_arraymap_set_(false) +{ + add_input(k_block_input, NodeValue::k_none, + InputFlags(k_input_flag_array | k_input_flag_not_keyframable | + k_input_flag_hidden | k_input_flag_ignore_invalidations)); + + add_input(k_muted_input, NodeValue::k_boolean, false, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable)); + + add_input(k_array_map_input, NodeValue::k_binary, + InputFlags(k_input_flag_static | k_input_flag_hidden | + k_input_flag_ignore_invalidations)); + + // Set default height + track_height_ = k_track_height_default; +} + +void Track::set_type(const Type &track_type) +{ + track_type_ = track_type; +} + +const Track::Type &Track::type() const +{ + return track_type_; +} + +std::string Track::name() const +{ + if (track_type_ == Track::k_video) { + return "Video Track " + std::to_string(index_); + } else if (track_type_ == Track::k_audio) { + return "Audio Track " + std::to_string(index_); + } else if (track_type_ == Track::k_subtitle) { + return "Subtitle Track " + std::to_string(index_); + } + + return "Track"; +} + +std::string Track::id() const +{ + return "org.olivevideoeditor.Olive.track"; +} + +std::vector Track::category() const +{ + return { k_category_timeline }; +} + +std::string Track::description() const +{ + return "Node for representing and processing a single array of Blocks sorted by time. Also represents the end of " + "a Sequence."; +} + +Node::ActiveElements Track::get_active_elements_at_time(const std::string &input, + const TimeRange &r) const +{ + if (input == k_block_input) { + if (is_muted() || blocks_.empty() || r.in() >= track_length() || + r.out() <= 0) { + return ActiveElements::k_no_elements; + } else { + int start = get_block_index_at_time(r.in()); + int end = get_block_index_at_time(r.out()); + + if (start == -1) { + start = 0; + } + if (end == -1) { + end = int(blocks_.size()) - 1; + } + + if (blocks_.at(end)->in() == r.out()) { + end--; + } + + ActiveElements a; + for (int i = start; i <= end; i++) { + Block *b = blocks_.at(i); + if (b->is_enabled() && (dynamic_cast(b) || + dynamic_cast(b))) { + a.add(get_array_index_from_cache_index(i)); + } + } + + if (a.elements().empty()) { + return ActiveElements::k_no_elements; + } else { + return a; + } + } + } else { + return super::get_active_elements_at_time(input, r); + } +} + +void Track::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (this->type() == Track::k_video) { + // Just pass straight through + NodeValueArray a = value.at(k_block_input).to_array(); + if (!a.empty()) { + table->push(a.begin()->second); + } + } else if (this->type() == Track::k_audio) { + // Audio + process_audio_track(value, globals, table); + } +} + +TimeRange Track::input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const +{ + if (input == k_block_input && element >= 0) { + int cache_index = get_cache_index_from_array_index(element); + + if (cache_index > -1) { + TimeRange r = input_time; + Block *b = blocks_.at(cache_index); + + if (clamp) { + r.set_range(std::max(r.in(), b->in()), + std::min(r.out(), b->out())); + } + + return transform_range_for_block(b, r); + } + } + + return Node::input_time_adjustment(input, element, input_time, clamp); +} + +TimeRange Track::output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const +{ + if (input == k_block_input && element >= 0) { + int cache_index = get_cache_index_from_array_index(element); + + if (cache_index > -1) { + return transform_range_from_block(blocks_.at(cache_index), input_time); + } + } + + return Node::output_time_adjustment(input, element, input_time); +} + +const double &Track::get_track_height() const +{ + return track_height_; +} + +void Track::set_track_height(const double &height) +{ + track_height_ = height; +} + +bool Track::load_custom(XmlStreamReader *reader, SerializedData *data) +{ + ignore_arraymap_set_ = true; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "height") { + this->set_track_height( + strtod(reader->read_element_text().c_str(), nullptr)); + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void Track::save_custom(XmlStreamWriter *writer) const +{ + writer->write_text_element("height", + number_to_string(this->get_track_height())); +} + +void Track::PostLoadEvent(SerializedData *data) +{ + ignore_arraymap_set_ = false; + refresh_block_cache_from_array_map(); +} + +void Track::InputValueChangedEvent(const std::string &input, int element) +{ + (void) element; + + if (input == k_array_map_input) { + if (ignore_arraymap_ > 0) { + ignore_arraymap_--; + } else { + refresh_block_cache_from_array_map(); + } + } +} + +void Track::retranslate() +{ + super::retranslate(); + + set_input_name(k_block_input, "Blocks"); + set_input_name(k_muted_input, "Muted"); +} + +void Track::set_index(const int &index) +{ + index_ = index; +} + +Block *Track::block_containing_time(const Rational &time) const +{ + for (Block *block : blocks_) { + if (block->in() < time && block->out() > time) { + return block; + } else if (block->out() == time) { + break; + } + } + + return nullptr; +} + +Block *Track::nearest_block_before(const Rational &time) const +{ + for (Block *block : blocks_) { + // Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block + if (block->in() == time) { + break; + } + + if (block->out() >= time) { + return block; + } + } + + return nullptr; +} + +Block *Track::nearest_block_before_or_at(const Rational &time) const +{ + for (Block *block : blocks_) { + // Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block + if (block->out() > time) { + return block; + } + } + + return nullptr; +} + +Block *Track::nearest_block_after_or_at(const Rational &time) const +{ + for (Block *block : blocks_) { + // Blocks are sorted by time, so the first Block after this time is the correct Block + if (block->in() >= time) { + return block; + } + } + + return nullptr; +} + +Block *Track::nearest_block_after(const Rational &time) const +{ + for (Block *block : blocks_) { + // Blocks are sorted by time, so the first Block after this time is the correct Block + if (block->in() > time) { + return block; + } + } + + return nullptr; +} + +bool Track::is_range_free(const TimeRange &range) const +{ + Block *b = nearest_block_before_or_at(range.in()); + if (!b) { + // No block here, assume track is empty here + return true; + } + + if (!dynamic_cast(b)) { + // There's a block at or around the start point that isn't a gap, range is not free + return false; + } + + while ((b = b->next())) { + if (b->in() >= range.out()) { + // This block is after the range, no longer relevant + break; + } else if (!dynamic_cast(b)) { + // Found a block in this range, range is not free + return false; + } + } + + // If we get here, we couldn't find anything in the way of this range + return true; +} + +void Track::invalidate_cache(const TimeRange &range, const std::string &from, + int element, InvalidateCacheOptions options) +{ + TimeRange limited; + + const Block *b; + + if (from == k_block_input && element >= 0 && + (b = dynamic_cast(get_connected_output(from, element))) && + !(options.count("lengthevent") && options.at("lengthevent").to_bool())) { + // Limit the range signal to the corresponding block + TimeRange transformed = transform_range_from_block(b, range); + + if (transformed.out() <= b->in() || transformed.in() >= b->out()) { + return; + } + + limited = TimeRange(std::max(transformed.in(), b->in()), + std::min(transformed.out(), b->out())); + } else { + limited = range; + } + + // NOTE: For now, I figure we drop this key, but we may find in the future that it's advantageous + // to keep it + options.erase("lengthevent"); + + Node::invalidate_cache(limited, from, element, options); +} + +void Track::insert_block_before(Block *block, Block *after) +{ + if (!after) { + append_block(block); + } else { + insert_block_at_index(block, index_of(blocks_, after)); + } +} + +void Track::insert_block_after(Block *block, Block *before) +{ + if (!before) { + prepend_block(block); + } else { + int before_index = index_of(blocks_, before); + + assert(before_index >= 0); + + insert_block_at_index(block, before_index + 1); + } +} + +void Track::prepend_block(Block *block) +{ + insert_block_at_index(block, 0); +} + +void Track::insert_block_at_index(Block *block, int index) +{ + // Set track + assert(block->track() == nullptr); + block->set_track(this); + + // Update array + int array_index = connect_block(block); + blocks_.insert(blocks_.begin() + index, block); + block_array_indexes_.insert(block_array_indexes_.begin() + index, array_index); + + // Handle previous/next + Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr; + Block *next = (index < int(blocks_.size()) - 1) ? blocks_.at(index + 1) : + nullptr; + Block::set_previous_next(previous, block); + Block::set_previous_next(block, next); + + // Update in/out + update_in_out_from(index); + + Node::invalidate_cache(TimeRange(block->in(), track_length()), k_block_input); + + update_array_map(); +} + +void Track::append_block(Block *block) +{ + insert_block_at_index(block, int(blocks_.size())); +} + +void Track::ripple_remove_block(Block *block) +{ + Rational remove_in = block->in(); + Rational remove_out = block->out(); + + // Set track + assert(block->track() == this); + block->set_track(nullptr); + + // Update array + int index = index_of(blocks_, block); + assert(index != -1); + + int array_index = block_array_indexes_.at(index); + + blocks_.erase(blocks_.begin() + index); + block_array_indexes_.erase(block_array_indexes_.begin() + index); + + Node::disconnect_edge(block, NodeInput(this, k_block_input, array_index)); + empty_inputs_.push_back(array_index); + + // Handle previous/next + Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr; + Block *next = (index < int(blocks_.size())) ? blocks_.at(index) : nullptr; + Block::set_previous_next(previous, next); + block->set_previous(nullptr); + block->set_next(nullptr); + block->set_in(0); + block->set_out(block->length()); + + // Update in/outs + update_in_out_from(index); + + Node::invalidate_cache( + TimeRange(remove_in, std::max(track_length(), remove_out)), k_block_input); + + update_array_map(); +} + +void Track::replace_block(Block *old, Block *replace) +{ + // Set track + assert(old->track() == this); + old->set_track(nullptr); + + assert(replace->track() == nullptr); + replace->set_track(this); + + // Update array + int cache_index = index_of(blocks_, old); + int index_of_old_block = get_array_index_from_cache_index(cache_index); + + ignore_block_disconnect_++; + disconnect_edge(old, NodeInput(this, k_block_input, index_of_old_block)); + ignore_block_disconnect_--; + connect_edge(replace, NodeInput(this, k_block_input, index_of_old_block)); + blocks_[cache_index] = replace; + + // Handle previous/next + replace->set_previous(old->previous()); + replace->set_next(old->next()); + old->set_previous(nullptr); + old->set_next(nullptr); + if (replace->previous()) { + replace->previous()->set_next(replace); + } + if (replace->next()) { + replace->next()->set_previous(replace); + } + + if (old->length() == replace->length()) { + replace->set_in(replace->previous() ? replace->previous()->out() : 0); + replace->set_out(replace->in() + replace->length()); + + Node::invalidate_cache(TimeRange(replace->in(), replace->out()), + k_block_input); + } else { + // Update in/outs + update_in_out_from(cache_index); + + Node::invalidate_cache(TimeRange(replace->in(), track_length()), + k_block_input); + } + + update_array_map(); +} + +Rational Track::track_length() const +{ + if (blocks_.empty()) { + return 0; + } else { + return blocks_.back()->out(); + } +} + +bool Track::is_muted() const +{ + return get_standard_value(k_muted_input).to_bool(); +} + +bool Track::is_locked() const +{ + return locked_; +} + +void Track::set_muted(bool e) +{ + set_standard_value(k_muted_input, e); +} + +void Track::set_locked(bool e) +{ + locked_ = e; +} + +void Track::InputConnectedEvent(const std::string &input, int element, Node *node) +{ + if (arraymap_invalid_ && input == k_block_input && element >= 0) { + refresh_block_cache_from_array_map(); + } +} + +void Track::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + Node::InputDisconnectedEvent(input, element, output); + + // Keep the block cache consistent when a block edge is removed outside + // the Track's own mutating operations (e.g. the block is being deleted, + // or an undo command is detaching the whole track from the graph). + // Without this, blocks_ keeps a dangling pointer and later readers such + // as track_length() walk into freed memory. + // + // Only the volatile cache is trimmed here; the persistent array map is + // deliberately left alone so that undo can re-attach the blocks from it + // (InputConnectedEvent rebuilds the cache while arraymap_invalid_ is + // set). + if (input == k_block_input && ignore_block_disconnect_ == 0) { + const int index = index_of(blocks_, static_cast(output)); + if (index != -1) { + blocks_.erase(blocks_.begin() + index); + block_array_indexes_.erase(block_array_indexes_.begin() + index); + arraymap_invalid_ = true; + + Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr; + Block *next = (index < int(blocks_.size())) ? blocks_.at(index) : nullptr; + Block::set_previous_next(previous, next); + + update_in_out_from(index); + } + } +} + +void Track::update_in_out_from(int index) +{ + // Find block just before this one to find the last out point + Rational last_out = (index == 0) ? 0 : blocks_.at(index - 1)->out(); + + // Iterate through all blocks updating their in/outs + for (int i = index; i < int(blocks_.size()); i++) { + Block *b = blocks_.at(i); + + b->set_in(last_out); + + last_out += b->length(); + + b->set_out(last_out); + } +} + +int Track::get_array_index_from_block(Block *block) const +{ + return block_array_indexes_.at(index_of(blocks_, block)); +} + +int Track::get_array_index_from_cache_index(int index) const +{ + return block_array_indexes_.at(index); +} + +int Track::get_cache_index_from_array_index(int index) const +{ + auto it = std::find(block_array_indexes_.begin(), block_array_indexes_.end(), + uint32_t(index)); + return (it == block_array_indexes_.end()) ? -1 : + int(it - block_array_indexes_.begin()); +} + +int Track::get_block_index_at_time(const Rational &time) const +{ + if (time < 0 || time >= track_length()) { + return -1; + } + + // Use binary search to find block at time + int low = 0; + int high = int(blocks_.size()) - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + + Block *block = blocks_.at(mid); + if (block->in() <= time && block->out() > time) { + return mid; + } else if (block->out() <= time) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return -1; +} + +void Track::process_audio_track(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + const TimeRange &range = globals.time(); + + // All these blocks will need to output to a buffer so we create one here + SampleBuffer block_range_buffer(globals.aparams(), range.length()); + block_range_buffer.silence(); + + // Loop through active blocks retrieving their audio + NodeValueArray arr = value.at(k_block_input).to_array(); + + for (auto it = arr.cbegin(); it != arr.cend(); it++) { + Block *b = blocks_.at(get_cache_index_from_array_index(it->first)); + + TimeRange range_for_block(std::max(b->in(), range.in()), + std::min(b->out(), range.out())); + + int64_t source_offset = 0; + int64_t destination_offset = globals.aparams().time_to_samples( + range_for_block.in() - range.in()); + int64_t max_dest_sz = + globals.aparams().time_to_samples(range_for_block.length()); + + // Destination buffer + SampleBuffer samples_from_this_block = it->second.to_samples(); + + if (samples_from_this_block.is_allocated()) { + // If this is a clip, we might have extra speed/reverse information + if (ClipBlock *clip_cast = dynamic_cast(b)) { + double speed_value = clip_cast->speed(); + bool reversed = clip_cast->reverse(); + + if (speed_value == 0.0) { + // Just silence, don't think there's any other practical application of 0 speed audio + samples_from_this_block.silence(); + } else if (!(std::abs(speed_value - 1.0) * 100000.0 <= + std::min(std::abs(speed_value), 1.0))) { + if (clip_cast->maintain_audio_pitch()) { + AudioProcessor processor; + + if (processor.open( + samples_from_this_block.audio_params(), + samples_from_this_block.audio_params(), + speed_value)) { + AudioProcessor::Buffer out; + + // FIXME: This is not the best way to do this, the TempoProcessor works best + // when it's given a continuous stream of audio, which is challenging + // in our current "modular" audio system. This should still work reasonably + // well on export (assuming audio is all generated at once on export), but + // users may hear clicks and pops in the audio during preview due to this + // approach. + int r = processor.convert( + samples_from_this_block.to_raw_ptrs().data(), + samples_from_this_block.sample_count(), + nullptr); + + if (r < 0) { + fprintf(stderr, + "Failed to change tempo of audio: %d\n", + r); + } else { + processor.flush(); + + processor.convert(nullptr, 0, &out); + + if (!out.empty()) { + int nb_samples = + int(out.front().size()) * + samples_from_this_block.audio_params() + .bytes_per_sample_per_channel(); + + if (nb_samples) { + SampleBuffer new_samples( + samples_from_this_block + .audio_params(), + nb_samples); + + for (int i = 0; i < int(out.size()); i++) { + memcpy(new_samples.data(i), + out[i].data(), + out[i].size()); + } + + samples_from_this_block = new_samples; + } + } + } + } + } else { + // Multiply time + samples_from_this_block.speed(speed_value); + } + } + + if (reversed) { + samples_from_this_block.reverse(); + } + } + + int64_t copy_length = std::min( + max_dest_sz, + int64_t(samples_from_this_block.sample_count() - source_offset)); + + // Copy samples into destination buffer + for (int i = 0; + i < samples_from_this_block.audio_params().channel_count(); + i++) { + block_range_buffer.set( + i, samples_from_this_block.data(i) + source_offset, + destination_offset, copy_length); + } + } + } + + table->push(NodeValue::k_samples, Variant::from_value(block_range_buffer), + this); +} + +int Track::connect_block(Block *b) +{ + if (!empty_inputs_.empty()) { + int index = empty_inputs_.front(); + empty_inputs_.pop_front(); + + Node::connect_edge(b, NodeInput(this, k_block_input, index)); + + return index; + } else { + int old_sz = input_array_size(k_block_input); + input_array_append(k_block_input); + Node::connect_edge(b, NodeInput(this, k_block_input, old_sz)); + return old_sz; + } +} + +void Track::update_array_map() +{ + ignore_arraymap_++; + set_standard_value( + k_array_map_input, + ByteArray(reinterpret_cast(block_array_indexes_.data()), + reinterpret_cast(block_array_indexes_.data()) + + block_array_indexes_.size() * sizeof(uint32_t))); +} + +void Track::refresh_block_cache_from_array_map() +{ + if (ignore_arraymap_set_) { + return; + } + + // Disconnecting any existing blocks + for (Block *b : blocks_) { + assert(b->track() == this); + b->set_track(nullptr); + b->set_previous(nullptr); + b->set_next(nullptr); + b->set_in(0); + b->set_out(b->length()); + } + + ByteArray bytes = get_standard_value(k_array_map_input).to_byte_array(); + block_array_indexes_.resize(bytes.size() / sizeof(uint32_t)); + memcpy(block_array_indexes_.data(), bytes.data(), bytes.size()); + blocks_.clear(); + blocks_.reserve(block_array_indexes_.size()); + + Block *prev = nullptr; + arraymap_invalid_ = false; + + for (int i = 0; i < int(block_array_indexes_.size()); i++) { + Block *b = static_cast( + get_connected_output(k_block_input, block_array_indexes_.at(i))); + + Block::set_previous_next(prev, b); + + if (b) { + b->set_track(this); + + blocks_.push_back(b); + prev = b; + } else { + block_array_indexes_.resize(i); + arraymap_invalid_ = true; + break; + } + } + + if (prev) { + prev->set_next(nullptr); + } + + update_in_out_from(0); +} + +void Track::block_length_changed(Block *b) +{ + update_in_out_from(index_of(blocks_, b)); +} + +} diff --git a/src/node/src/output/track/track.h b/src/node/src/output/track/track.h new file mode 100644 index 000000000..3a0308c3a --- /dev/null +++ b/src/node/src/output/track/track.h @@ -0,0 +1,509 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TRACK_H +#define OAK_TRACK_H + +#include +#include +#include +#include +#include + +#include "block/block.h" + +namespace olive +{ + +class Sequence; + +/** + * @brief A time traversal Node for sorting through one channel/track of Blocks + */ +class Track : public Node { +public: + enum Type { k_none = -1, k_video, k_audio, k_subtitle, k_count }; + + Track(); + + NODE_DEFAULT_FUNCTIONS(Track) + + const Track::Type &type() const; + void set_type(const Track::Type &track_type); + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual ActiveElements + get_active_elements_at_time(const std::string &input, + const TimeRange &r) const override; + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const override; + + virtual TimeRange + output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const override; + + static Rational transform_time_for_block(const Block *block, + const Rational &time) + { + if (time == RATIONAL_MAX || time == RATIONAL_MIN) { + return time; + } + + return time - block->in(); + } + + static TimeRange transform_range_for_block(const Block *block, + const TimeRange &range) + { + return TimeRange(transform_time_for_block(block, range.in()), + transform_time_for_block(block, range.out())); + } + + static Rational transform_time_from_block(const Block *block, + const Rational &time) + { + if (time == RATIONAL_MAX || time == RATIONAL_MIN) { + return time; + } + + return time + block->in(); + } + + static TimeRange transform_range_from_block(const Block *block, + const TimeRange &range) + { + return TimeRange(transform_time_from_block(block, range.in()), + transform_time_from_block(block, range.out())); + } + + const double &get_track_height() const; + void set_track_height(const double &height); + + int get_track_height_in_pixels() const + { + return internal_height_to_pixel_height(get_track_height()); + } + + void set_track_height_in_pixels(int h) + { + set_track_height(pixel_height_to_internal_height(h)); + } + + virtual bool load_custom(XmlStreamReader *reader, + SerializedData *data) override; + virtual void save_custom(XmlStreamWriter *writer) const override; + virtual void PostLoadEvent(SerializedData *data) override; + + /** + * @brief Replacement for QFontMetrics(QFont()).height() + * + * oaknode no longer depends on Qt, so the default font height can no + * longer be queried from QFontMetrics. Use the height of Qt's default + * application font (9pt -> 12px ascent+descent plus leading = 13px), + * which is what the Qt build returned on standard 96 DPI desktops. + */ + static int default_font_height() + { + return 13; + } + + static int internal_height_to_pixel_height(double h) + { + return int(std::lround(h * default_font_height())); + } + + static double pixel_height_to_internal_height(int h) + { + return double(h) / double(default_font_height()); + } + + static int get_default_track_height_in_pixels() + { + return internal_height_to_pixel_height(k_track_height_default); + } + + static int get_minimum_track_height_in_pixels() + { + return internal_height_to_pixel_height(k_track_height_minimum); + } + + virtual void retranslate() override; + + class Reference { + public: + Reference() + : type_(k_none) + , index_(-1) + { + } + + Reference(const Track::Type &type, const int &index) + : type_(type) + , index_(index) + { + } + + const Track::Type &type() const + { + return type_; + } + + const int &index() const + { + return index_; + } + + bool operator==(const Reference &ref) const + { + return type_ == ref.type_ && index_ == ref.index_; + } + + bool operator!=(const Reference &ref) const + { + return !(*this == ref); + } + + bool operator<(const Track::Reference &rhs) const + { + if (type_ != rhs.type_) { + return type_ < rhs.type_; + } + + return index_ < rhs.index_; + } + + std::string to_string() const + { + std::string type_string = type_to_string(type_); + if (type_string.empty()) { + return std::string(); + } else { + return type_string + ":" + std::to_string(index_); + } + } + + /// For IDs that shouldn't change between localizations + static std::string type_to_string(Type type) + { + switch (type) { + case k_video: + return "v"; + case k_audio: + return "a"; + case k_subtitle: + return "s"; + case k_count: + case k_none: + break; + } + + return std::string(); + } + + /// For human-facing strings + static std::string type_to_translated_string(Type type) + { + switch (type) { + case k_video: + return "V"; + case k_audio: + return "A"; + case k_subtitle: + return "S"; + case k_count: + case k_none: + break; + } + + return std::string(); + } + + static Type type_from_string(const std::string &s) + { + if (s.size() >= 3) { + if (s.at(1) == ':') { + if (s.at(0) == 'v') { + // Video stream + return Track::k_video; + } else if (s.at(0) == 'a') { + // Audio stream + return Track::k_audio; + } else if (s.at(0) == 's') { + // Subtitle stream + return Track::k_subtitle; + } + } + } + + return Track::k_none; + } + + static Reference from_string(const std::string &s) + { + Reference ref; + Type parse_type = type_from_string(s); + + if (parse_type != Track::k_none) { + const char *index_str = s.c_str() + 2; + char *end = nullptr; + long parse_index = std::strtol(index_str, &end, 10); + bool ok = (end != index_str && *end == '\0'); + + if (ok) { + ref.type_ = parse_type; + ref.index_ = int(parse_index); + } + } + + return ref; + } + + bool is_valid() const + { + return type_ > k_none && type_ < k_count && index_ >= 0; + } + + private: + Track::Type type_; + + int index_; + }; + + Reference to_reference() const + { + return Reference(type(), index()); + } + + const int &index() const + { + return index_; + } + + void set_index(const int &index); + + /** + * @brief Returns the block that starts BEFORE (not AT) and ends AFTER (not AT) a time + * + * Catches the first block that matches `block.in < time && block.out > time` or nullptr if any + * block starts/ends precisely at that time or the time exceeds the track length. + */ + Block *block_containing_time(const Rational &time) const; + + /** + * @brief Returns the block that starts BEFORE a given time and ends either AFTER or AT that time + * + * @return Catches the first block that matches `block.out >= time` or nullptr if this time + * exceeds the track length. + */ + Block *nearest_block_before(const Rational &time) const; + + /** + * @brief Returns the block that starts BEFORE or AT a given time. + * + * @return Catches the first block that matches `block.out > time` or nullptr if this time + * exceeds the track length. + */ + Block *nearest_block_before_or_at(const Rational &time) const; + + /** + * @brief Returns the block that starts either AT a given time or the soonest block AFTER + * + * @return Catches the first block that matches `block.in >= time` or nullptr if this time + * exceeds the track length. + */ + Block *nearest_block_after_or_at(const Rational &time) const; + + /** + * @brief Returns the block that starts AFTER the given time (but never AT the given time) + * + * @return Catches the first block that matches `block.in > time` or nullptr if this time + * exceeds the track length. + */ + Block *nearest_block_after(const Rational &time) const; + + /* + * @brief Returns whether a time range is empty or only has a gap + */ + bool is_range_free(const TimeRange &range) const; + + const std::vector &blocks() const + { + return blocks_; + } + + virtual void invalidate_cache(const TimeRange &range, const std::string &from, + int element, + InvalidateCacheOptions options) override; + + Block *visible_block_at_time(const Rational &t) const + { + int index = get_block_index_at_time(t); + return (index == -1) ? nullptr : blocks_.at(index); + } + + /** + * @brief Adds Block `block` at the very beginning of the Sequence before all other clips + */ + void prepend_block(Block *block); + + /** + * @brief Inserts Block `block` at a specific index (0 is the start of the timeline) + * + * If the index == 0, this function does the same as PrependBlock(). If the index >= the current number of blocks, + * this function is the same as AppendBlock(). + */ + void insert_block_at_index(Block *block, int index); + + /** + * @brief Inserts Block after another Block + * + * Equivalent to calling InsertBlockBetweenBlocks(block, before, before->next()) + */ + void insert_block_after(Block *block, Block *before); + + /** + * @brief Inserts Block before another Block + */ + void insert_block_before(Block *block, Block *after); + + /** + * @brief Adds Block `block` at the very end of the Sequence after all other clips + */ + void append_block(Block *block); + + /** + * @brief Removes a Block pushing all subsequent Blocks earlier to take up the space + */ + void ripple_remove_block(Block *block); + + /** + * @brief Replaces Block `old` with Block `replace` + * + * Both blocks must have equal lengths. + */ + void replace_block(Block *old, Block *replace); + + Rational track_length() const; + + bool is_muted() const; + + bool is_locked() const; + + int get_array_index_from_block(Block *block) const; + + Sequence *sequence() const + { + return sequence_; + } + + void set_sequence(Sequence *sequence) + { + sequence_ = sequence; + } + + void set_muted(bool e); + + void set_locked(bool e); + + /** + * @brief Block length change notification + * + * Formerly a private slot connected to Block::length_changed and driven + * by sender(); now a plain member function that the caller (facade / + * event layer) invokes with the Block whose length changed. + */ + void block_length_changed(Block *block); + + static const double k_track_height_default; + static const double k_track_height_minimum; + static const double k_track_height_interval; + + static const std::string k_block_input; + static const std::string k_muted_input; + static const std::string k_array_map_input; + +protected: + virtual void InputConnectedEvent(const std::string &input, int element, + Node *node) override; + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + +private: + void update_in_out_from(int index); + + int get_array_index_from_cache_index(int index) const; + + int get_cache_index_from_array_index(int index) const; + + int get_block_index_at_time(const Rational &time) const; + + void process_audio_track(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const; + + int connect_block(Block *b); + + void update_array_map(); + + void refresh_block_cache_from_array_map(); + + core::TimeRangeList block_length_pending_invalidations_; + + std::vector blocks_; + std::vector block_array_indexes_; + + std::list empty_inputs_; + + Track::Type track_type_; + + double track_height_; + + int index_; + + bool locked_; + + Sequence *sequence_; + + int ignore_arraymap_; + bool arraymap_invalid_; + bool ignore_arraymap_set_; + + /** + * @brief Nestable guard suppressing the block-cache maintenance in + * InputDisconnectedEvent while the Track itself is rewiring block edges + * (e.g. replace_block), where the cache update is handled explicitly. + */ + int ignore_block_disconnect_ = 0; +}; + +} + +#endif // OAK_TRACK_H diff --git a/src/node/src/output/track/tracklist.cpp b/src/node/src/output/track/tracklist.cpp new file mode 100644 index 000000000..8d7a1fa75 --- /dev/null +++ b/src/node/src/output/track/tracklist.cpp @@ -0,0 +1,173 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "tracklist.h" + +#include + +#include "factory.h" +#include "math/math/math.h" +#include "math/merge/merge.h" +#include "output/viewer/viewer.h" +#include "project/sequence/sequence.h" + +namespace olive +{ + +TrackList::TrackList(Sequence *parent, const Track::Type &type, + const std::string &track_input) + : parent_(parent) + , track_input_(track_input) + , total_length_(0) + , type_(type) +{ +} + +Track *TrackList::get_track_at(int index) const +{ + if (index >= 0 && index < int(track_cache_.size())) { + return track_cache_.at(index); + } else { + return nullptr; + } +} + +void TrackList::track_connected(Node *node, int element) +{ + if (element == -1) { + parent()->invalidate_all(track_input(), element); + return; + } + + Track *track = dynamic_cast(node); + + if (!track) { + return; + } + + // Determine where in the cache this block will be + int cache_index = -1; + for (int i = element + 1; i < array_size(); i++) { + // Find next track because this will be the index we insert at + cache_index = get_cache_index_from_array_index(i); + + if (cache_index >= 0) { + break; + } + } + + // If there was no next, this will be inserted at the end + if (cache_index == -1) { + cache_index = int(track_cache_.size()); + } + + track_cache_.insert(track_cache_.begin() + cache_index, track); + track_array_indexes_.insert(track_array_indexes_.begin() + cache_index, + element); + + // Update track indexes in the list (including this track) + update_track_indexes_from(cache_index); + + track->set_type(type_); + track->set_sequence(parent()); + + update_total_length(); +} + +void TrackList::track_disconnected(Node *node, int element) +{ + if (element == -1) { + // User has replaced the entire array, we will invalidate everything + parent()->invalidate_all(track_input(), element); + return; + } + + Track *track = dynamic_cast(node); + + if (!track) { + return; + } + + int cache_index = get_cache_index_from_array_index(element); + + // Remove track here + track_cache_.erase(track_cache_.begin() + cache_index); + track_array_indexes_.erase(track_array_indexes_.begin() + cache_index); + + // Update indices for all subsequent tracks + update_track_indexes_from(cache_index); + + track->set_index(-1); + track->set_type(Track::k_none); + track->set_sequence(nullptr); + + update_total_length(); +} + +void TrackList::update_track_indexes_from(int index) +{ + for (int i = index; i < int(track_cache_.size()); i++) { + track_cache_.at(i)->set_index(i); + } +} + +Project *TrackList::get_parent_graph() const +{ + return parent()->parent(); +} + +const std::string &TrackList::track_input() const +{ + return track_input_; +} + +NodeInput TrackList::track_input(int element) const +{ + return NodeInput(parent(), track_input(), element); +} + +int TrackList::array_size() const +{ + return parent()->input_array_size(track_input()); +} + +void TrackList::array_append() +{ + parent()->input_array_append(track_input()); +} + +void TrackList::array_remove_last() +{ + parent()->input_array_remove_last(track_input()); +} + +void TrackList::update_total_length() +{ + total_length_ = 0; + + for (Track *track : track_cache_) { + if (track) { + total_length_ = std::max(total_length_, track->track_length()); + } + } +} + +} diff --git a/src/node/src/output/track/tracklist.h b/src/node/src/output/track/tracklist.h new file mode 100644 index 000000000..06383eb00 --- /dev/null +++ b/src/node/src/output/track/tracklist.h @@ -0,0 +1,142 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TRACKLIST_H +#define OAK_TRACKLIST_H + +#include +#include +#include + +#include "output/track/track.h" +#include "timeline/timelinecommon.h" + +namespace olive +{ + +class Sequence; + +class TrackList { +public: + TrackList(Sequence *parent, const Track::Type &type, + const std::string &track_input); + + const Track::Type &type() const + { + return type_; + } + + const std::vector &get_tracks() const + { + return track_cache_; + } + + Track *get_track_at(int index) const; + + const Rational &get_total_length() const + { + return total_length_; + } + + int get_track_count() const + { + return track_cache_.size(); + } + + Project *get_parent_graph() const; + + const std::string &track_input() const; + NodeInput track_input(int element) const; + + Sequence *parent() const + { + return parent_; + } + + void set_parent(Sequence *parent) + { + parent_ = parent; + } + + int array_size() const; + + void array_append(); + void array_remove_last(); + + int get_array_index_from_cache_index(int index) const + { + return track_array_indexes_.at(index); + } + + int get_cache_index_from_array_index(int index) const + { + auto it = std::find(track_array_indexes_.begin(), + track_array_indexes_.end(), index); + return (it == track_array_indexes_.end()) ? + -1 : + int(it - track_array_indexes_.begin()); + } + + /** + * @brief Handler for when the track connection is added + * + * Formerly a slot connected to the Sequence's input_connected signal; + * now invoked directly by the caller (facade / event layer). + */ + void track_connected(Node *node, int element); + + /** + * @brief Handler for when the track connection is removed + * + * Formerly a slot connected to the Sequence's input_disconnected signal; + * now invoked directly by the caller (facade / event layer). + */ + void track_disconnected(Node *node, int element); + + /** + * @brief Handler for when any of the track's length changes so we can update the length of the tracklist + * + * Formerly a slot connected to Track::track_length_changed; now invoked + * directly by the caller (facade / event layer). + */ + void update_total_length(); + +private: + void update_track_indexes_from(int index); + + /** + * @brief A cache of connected Tracks + */ + std::vector track_cache_; + std::vector track_array_indexes_; + + Sequence *parent_; + + std::string track_input_; + + Rational total_length_; + + enum Track::Type type_; +}; + +} + +#endif // OAK_TRACKLIST_H diff --git a/src/node/src/output/viewer/CMakeLists.txt b/src/node/src/output/viewer/CMakeLists.txt new file mode 100644 index 000000000..876634308 --- /dev/null +++ b/src/node/src/output/viewer/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/output/viewer/viewer.h + node/output/viewer/viewer.cpp + PARENT_SCOPE +) diff --git a/src/node/src/output/viewer/viewer.cpp b/src/node/src/output/viewer/viewer.cpp new file mode 100644 index 000000000..3003fab7a --- /dev/null +++ b/src/node/src/output/viewer/viewer.cpp @@ -0,0 +1,615 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "viewer.h" + +#include "config/config.h" +#include "coreengine.h" +#include "traverser.h" +#include "olive/core/util/timecodefunctions.h" + +namespace olive +{ + +using core::Timecode; + +const std::string ViewerOutput::k_video_params_input = "video_param_in"; +const std::string ViewerOutput::k_audio_params_input = "audio_param_in"; +const std::string ViewerOutput::k_subtitle_params_input = "subtitle_param_in"; +const std::string ViewerOutput::k_texture_input = "tex_in"; +const std::string ViewerOutput::k_samples_input = "samples_in"; + +const SampleFormat ViewerOutput::k_default_sample_format = SampleFormat::f32_p; + +#define super Node + +ViewerOutput::ViewerOutput(bool create_buffer_inputs, + bool create_default_streams) + : last_length_(0) + , video_length_(0) + , audio_length_(0) + , autocache_input_video_(true) + , autocache_input_audio_(false) + , waveform_requests_enabled_(false) +{ + add_input(k_video_params_input, NodeValue::k_video_params, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable | + k_input_flag_array | k_input_flag_hidden)); + + add_input(k_audio_params_input, NodeValue::k_audio_params, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable | + k_input_flag_array | k_input_flag_hidden)); + + add_input(k_subtitle_params_input, NodeValue::k_subtitle_params, + InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable | + k_input_flag_array | k_input_flag_hidden)); + + if (create_buffer_inputs) { + add_input(k_texture_input, NodeValue::k_texture, + InputFlags(k_input_flag_not_keyframable)); + add_input(k_samples_input, NodeValue::k_samples, + InputFlags(k_input_flag_not_keyframable)); + } + + if (create_default_streams) { + add_stream(Track::k_video, Variant()); + add_stream(Track::k_audio, Variant()); + set_default_parameters(); + } + + set_flag(k_dont_show_in_param_view); + + workarea_ = new TimelineWorkArea(this); + markers_ = new TimelineMarkerList(this); +} + +std::string ViewerOutput::name() const +{ + return "Viewer"; +} + +std::string ViewerOutput::id() const +{ + return "org.olivevideoeditor.Olive.vieweroutput"; +} + +std::vector ViewerOutput::category() const +{ + return { k_category_output }; +} + +std::string ViewerOutput::description() const +{ + return "Interface between a Viewer panel and the node system."; +} + +Variant ViewerOutput::data(const DataType &d) const +{ + switch (d) { + case duration: { + Rational using_timebase; + Timecode::Display using_display = + EngineCore::instance()->get_timecode_display(); + + // Get first enabled streams + VideoParams video = get_first_enabled_video_stream(); + AudioParams audio = get_first_enabled_audio_stream(); + SubtitleParams sub = get_first_enabled_subtitle_stream(); + + if (video.is_valid() && + video.video_type() != VideoParams::k_video_type_still) { + // Prioritize video + using_timebase = video.frame_rate_as_time_base(); + } else if (audio.is_valid()) { + // Use audio as a backup + // If we're showing in a timecode, we prefer showing audio in seconds instead + if (using_display == Timecode::k_timecode_drop_frame || + using_display == Timecode::k_timecode_non_drop_frame) { + using_display = Timecode::k_timecode_seconds; + } + + using_timebase = audio.sample_rate_as_time_base(); + } else if (sub.is_valid()) { + using_timebase = + OAK_CONFIG("DefaultSequenceFrameRate").value(); + } + + if (!using_timebase.isNull()) { + // Return time transformed to timecode + return Timecode::time_to_timecode(get_length(), using_timebase, + using_display); + } + break; + } + case frequency_rate: { + VideoParams video_stream; + + if (has_enabled_video_streams() && + (video_stream = get_first_enabled_video_stream()).video_type() != + VideoParams::k_video_type_still) { + // This is a video editor, prioritize video streams + return Variant(video_stream.frame_rate().to_double()).to_string() + + " FPS"; + } else if (has_enabled_audio_streams()) { + // No video streams, return audio + AudioParams audio_stream = get_first_enabled_audio_stream(); + return std::to_string(audio_stream.sample_rate()) + " Hz"; + } + break; + } + default: + break; + } + + return super::data(d); +} + +bool ViewerOutput::has_enabled_video_streams() const +{ + return get_first_enabled_video_stream().is_valid(); +} + +bool ViewerOutput::has_enabled_audio_streams() const +{ + return get_first_enabled_audio_stream().is_valid(); +} + +bool ViewerOutput::has_enabled_subtitle_streams() const +{ + return get_first_enabled_subtitle_stream().is_valid(); +} + +VideoParams ViewerOutput::get_first_enabled_video_stream() const +{ + int sz = get_video_stream_count(); + + for (int i = 0; i < sz; i++) { + VideoParams vp = get_video_params(i); + + if (vp.enabled()) { + return vp; + } + } + + return VideoParams(); +} + +AudioParams ViewerOutput::get_first_enabled_audio_stream() const +{ + int sz = get_audio_stream_count(); + + for (int i = 0; i < sz; i++) { + AudioParams ap = get_audio_params(i); + + if (ap.enabled()) { + return ap; + } + } + + return AudioParams(); +} + +SubtitleParams ViewerOutput::get_first_enabled_subtitle_stream() const +{ + int sz = get_subtitle_stream_count(); + + for (int i = 0; i < sz; i++) { + SubtitleParams sp = get_subtitle_params(i); + + if (sp.enabled()) { + return sp; + } + } + + return SubtitleParams(); +} + +void ViewerOutput::set_default_parameters() +{ + int width = OAK_CONFIG("DefaultSequenceWidth").to_int(); + int height = OAK_CONFIG("DefaultSequenceHeight").to_int(); + + set_video_params(VideoParams( + width, height, + OAK_CONFIG("DefaultSequenceFrameRate").value(), + static_cast( + OAK_CONFIG("OfflinePixelFormat").to_int()), + VideoParams::k_internal_channel_count, + OAK_CONFIG("DefaultSequencePixelAspect").value(), + OAK_CONFIG("DefaultSequenceInterlacing") + .value(), + 1)); + set_audio_params( + AudioParams(OAK_CONFIG("DefaultSequenceAudioFrequency").to_int(), + OAK_CONFIG("DefaultSequenceAudioLayout").to_u_long_long(), + k_default_sample_format)); +} + +void ViewerOutput::invalidate_cache(const TimeRange &range, + const std::string &from, int element, + InvalidateCacheOptions options) +{ + (void) element; + + if (Node *connected = get_connected_output(from, element)) { + if (from == k_texture_input) { + //connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); + if (autocache_input_video_) { + TimeRange max_range = input_time_adjustment( + from, element, TimeRange(0, get_video_length()), false); + connected->video_frame_cache()->request( + this, range.intersected(max_range)); + } + } else if (from == k_samples_input) { + TimeRange max_range = input_time_adjustment( + from, element, TimeRange(0, get_audio_length()), false); + if (waveform_requests_enabled_) { + connected->waveform_cache()->request( + this, range.intersected(max_range)); + } + if (autocache_input_audio_) { + connected->audio_playback_cache()->request( + this, range.intersected(max_range)); + } + } + } + + verify_length(); + + super::invalidate_cache(range, from, element, options); +} + +std::vector ViewerOutput::get_enabled_streams_as_references() const +{ + std::vector refs; + + { + int vp_sz = get_video_stream_count(); + + for (int i = 0; i < vp_sz; i++) { + if (get_video_params(i).enabled()) { + refs.push_back(Track::Reference(Track::k_video, i)); + } + } + } + + { + int ap_sz = get_audio_stream_count(); + + for (int i = 0; i < ap_sz; i++) { + if (get_audio_params(i).enabled()) { + refs.push_back(Track::Reference(Track::k_audio, i)); + } + } + } + + { + int sp_sz = get_subtitle_stream_count(); + + for (int i = 0; i < sp_sz; i++) { + if (get_subtitle_params(i).enabled()) { + refs.push_back(Track::Reference(Track::k_subtitle, i)); + } + } + } + + return refs; +} + +void ViewerOutput::retranslate() +{ + super::retranslate(); + + set_input_name(k_video_params_input, "Video Parameters"); + set_input_name(k_audio_params_input, "Audio Parameters"); + set_input_name(k_subtitle_params_input, "Subtitle Parameters"); + + if (has_input_with_id(k_texture_input)) { + set_input_name(k_texture_input, "Texture"); + } + + if (has_input_with_id(k_samples_input)) { + set_input_name(k_samples_input, "Samples"); + } +} + +void ViewerOutput::verify_length() +{ + video_length_ = verify_length_internal(Track::k_video); + + audio_length_ = verify_length_internal(Track::k_audio); + + Rational subtitle_length = verify_length_internal(Track::k_subtitle); + + Rational real_length = + std::max(subtitle_length, std::max(video_length_, audio_length_)); + + if (real_length != last_length_) { + last_length_ = real_length; + } +} + +void ViewerOutput::set_playhead(const Rational &t) +{ + playhead_ = t; +} + +void ViewerOutput::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + super::InputConnectedEvent(input, element, output); +} + +void ViewerOutput::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + super::InputDisconnectedEvent(input, element, output); +} + +Rational ViewerOutput::verify_length_internal(Track::Type type) const +{ + NodeTraverser traverser; + + switch (type) { + case Track::k_video: + if (is_input_connected(k_texture_input)) { + NodeValueTable t = traverser.generate_table( + get_connected_output(k_texture_input), TimeRange(0, 0)); + Rational r = + t.get(NodeValue::k_rational, "length").to_rational(); + if (!r.isNaN()) { + return r; + } + } + break; + case Track::k_audio: + if (is_input_connected(k_samples_input)) { + NodeValueTable t = traverser.generate_table( + get_connected_output(k_samples_input), TimeRange(0, 0)); + Rational r = + t.get(NodeValue::k_rational, "length").to_rational(); + if (!r.isNaN()) { + return r; + } + } + break; + case Track::k_none: + case Track::k_subtitle: + case Track::k_count: + break; + } + + return 0; +} + +Node *ViewerOutput::get_connected_texture_output() +{ + return get_connected_output(k_texture_input); +} + +Node::ValueHint ViewerOutput::get_connected_texture_value_hint() +{ + return get_value_hint_for_input(k_texture_input); +} + +Node *ViewerOutput::get_connected_sample_output() +{ + return get_connected_output(k_samples_input); +} + +Node::ValueHint ViewerOutput::get_connected_sample_value_hint() +{ + return get_value_hint_for_input(k_samples_input); +} + +void ViewerOutput::set_waveform_enabled(bool e) +{ + if ((waveform_requests_enabled_ = e)) { + if (Node *connected = this->get_connected_sample_output()) { + TimeRange max_range = input_time_adjustment( + k_samples_input, -1, TimeRange(0, get_audio_length()), false); + core::TimeRangeList invalid = + connected->waveform_cache()->get_invalidated_ranges(max_range); + for (const TimeRange &r : invalid) { + connected->waveform_cache()->request(this, r); + } + } + } +} + +void ViewerOutput::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + if (has_input_with_id(k_texture_input)) { + NodeValue repush = value.at(k_texture_input); + repush.set_tag(Track::Reference(Track::k_video, 0).to_string()); + table->push(repush); + } + if (has_input_with_id(k_samples_input)) { + NodeValue repush = value.at(k_samples_input); + repush.set_tag(Track::Reference(Track::k_audio, 0).to_string()); + table->push(repush); + } +} + +bool ViewerOutput::load_custom(XmlStreamReader *reader, SerializedData *data) +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "markers") { + if (!this->get_markers()->load(reader)) { + return false; + } + } else if (reader->name() == "workarea") { + if (!this->get_work_area()->load(reader)) { + return false; + } + } else { + reader->skip_current_element(); + } + } + + return true; +} + +void ViewerOutput::save_custom(XmlStreamWriter *writer) const +{ + writer->write_start_element("workarea"); + this->get_work_area()->save(writer); + writer->write_end_element(); // workarea + + writer->write_start_element("markers"); + this->get_markers()->save(writer); + writer->write_end_element(); // markers +} + +void ViewerOutput::InputValueChangedEvent(const std::string &input, int element) +{ + if (element == 0) { + if (input == k_video_params_input) { + VideoParams new_video_params = get_video_params(); + + cached_video_params_ = new_video_params; + + } else if (input == k_audio_params_input) { + AudioParams new_audio_params = get_audio_params(); + + cached_audio_params_ = new_audio_params; + } + } + + super::InputValueChangedEvent(input, element); +} + +void ViewerOutput::set_parameters_from_footage( + const std::vector footage) +{ + for (ViewerOutput *f : footage) { + std::vector video_streams = f->get_enabled_video_streams(); + std::vector audio_streams = f->get_enabled_audio_streams(); + + for (int i = 0; i < int(video_streams.size()); i++) { + const VideoParams &s = video_streams.at(i); + + bool found_video_params = false; + Rational using_timebase; + + if (s.video_type() == VideoParams::k_video_type_still) { + // If this is a still image, we'll use it's resolution but won't set + // `found_video_params` in case something with a frame rate comes along which we'll + // prioritize + if (i > 0) { + // Ignore still images past stream 0 + continue; + } + + using_timebase = get_video_params().time_base(); + } else { + using_timebase = s.frame_rate_as_time_base(); + found_video_params = true; + } + + set_video_params( + VideoParams(s.width(), s.height(), using_timebase, + static_cast( + OAK_CONFIG("OfflinePixelFormat").to_int()), + VideoParams::k_internal_channel_count, + s.pixel_aspect_ratio(), s.interlacing(), 1)); + + if (found_video_params) { + break; + } + } + + if (!audio_streams.empty()) { + const AudioParams &s = audio_streams.front(); + set_audio_params(AudioParams(s.sample_rate(), s.channel_layout(), + k_default_sample_format)); + } + } +} + +int ViewerOutput::add_stream(Track::Type type, const Variant &value) +{ + return set_stream(type, value, -1); +} + +int ViewerOutput::set_stream(Track::Type type, const Variant &value, + int index_in) +{ + std::string id; + + if (type == Track::k_video) { + id = k_video_params_input; + } else if (type == Track::k_audio) { + id = k_audio_params_input; + } else if (type == Track::k_subtitle) { + id = k_subtitle_params_input; + } else { + return -1; + } + + // Add another video/audio param to the array for this stream + int index = (index_in == -1) ? input_array_size(id) : index_in; + + if (index >= input_array_size(id)) { + input_array_resize(id, index + 1); + } + + set_standard_value(id, value, index); + + return index; +} + +std::vector ViewerOutput::get_enabled_video_streams() const +{ + std::vector streams; + + int vp_sz = get_video_stream_count(); + + for (int i = 0; i < vp_sz; i++) { + VideoParams vp = get_video_params(i); + + if (vp.enabled()) { + streams.push_back(vp); + } + } + + return streams; +} + +std::vector ViewerOutput::get_enabled_audio_streams() const +{ + std::vector streams; + + int ap_sz = get_audio_stream_count(); + + for (int i = 0; i < ap_sz; i++) { + AudioParams ap = get_audio_params(i); + + if (ap.enabled()) { + streams.push_back(ap); + } + } + + return streams; +} + +} diff --git a/src/node/src/output/viewer/viewer.h b/src/node/src/output/viewer/viewer.h new file mode 100644 index 000000000..3576e9b0e --- /dev/null +++ b/src/node/src/output/viewer/viewer.h @@ -0,0 +1,293 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_VIEWER_H +#define OAK_VIEWER_H + +#include + +#include "codec/encoder.h" +#include "node.h" +#include "output/track/track.h" +#include "render/audioplaybackcache.h" +#include "render/framehashcache.h" +#include "subtitleparams.h" +#include "videoparams.h" +#include "timeline/timelinemarker.h" +#include "timeline/timelineworkarea.h" + +namespace olive +{ + +class Footage; + +/** + * @brief A bridge between a node system and a ViewerPanel + * + * Receives update/time change signals from ViewerPanels and responds by sending them a texture of that frame + */ +class ViewerOutput : public Node { +public: + ViewerOutput(bool create_buffer_inputs = true, + bool create_default_streams = true); + + NODE_DEFAULT_FUNCTIONS(ViewerOutput) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual Variant data(const DataType &d) const override; + + void set_default_parameters(); + + void set_parameters_from_footage(const std::vector footage); + + virtual void invalidate_cache(const TimeRange &range, const std::string &from, + int element, + InvalidateCacheOptions options) override; + + VideoParams get_video_params(int index = 0) const + { + // This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway), + // but it does suppress a warning message that we don't need + if (index < input_array_size(k_video_params_input)) { + return get_standard_value(k_video_params_input, index) + .value(); + } else { + return VideoParams(); + } + } + + AudioParams get_audio_params(int index = 0) const + { + // This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway), + // but it does suppress a warning message that we don't need + if (index < input_array_size(k_audio_params_input)) { + return get_standard_value(k_audio_params_input, index) + .value(); + } else { + return AudioParams(); + } + } + + SubtitleParams get_subtitle_params(int index = 0) const + { + // This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway), + // but it does suppress a warning message that we don't need + if (index < input_array_size(k_subtitle_params_input)) { + return get_standard_value(k_subtitle_params_input, index) + .value(); + } else { + return SubtitleParams(); + } + } + + const Rational &get_playhead() + { + return playhead_; + } + + void set_video_params(const VideoParams &video, int index = 0) + { + set_standard_value(k_video_params_input, Variant::from_value(video), index); + } + + void set_audio_params(const AudioParams &audio, int index = 0) + { + set_standard_value(k_audio_params_input, Variant::from_value(audio), index); + } + + void set_subtitle_params(const SubtitleParams &subs, int index = 0) + { + set_standard_value(k_subtitle_params_input, Variant::from_value(subs), + index); + } + + int get_video_stream_count() const + { + return input_array_size(k_video_params_input); + } + + int get_audio_stream_count() const + { + return input_array_size(k_audio_params_input); + } + + int get_subtitle_stream_count() const + { + return input_array_size(k_subtitle_params_input); + } + + virtual int get_total_stream_count() const + { + return get_video_stream_count() + get_audio_stream_count() + + get_subtitle_stream_count(); + } + + const AudioWaveformCache *get_connected_waveform() + { + if (Node *n = get_connected_sample_output()) { + return n->waveform_cache(); + } else { + return nullptr; + } + } + + bool has_enabled_video_streams() const; + bool has_enabled_audio_streams() const; + bool has_enabled_subtitle_streams() const; + + VideoParams get_first_enabled_video_stream() const; + AudioParams get_first_enabled_audio_stream() const; + SubtitleParams get_first_enabled_subtitle_stream() const; + + const Rational &get_length() const + { + return last_length_; + } + const Rational &get_video_length() const + { + return video_length_; + } + const Rational &get_audio_length() const + { + return audio_length_; + } + + TimelineWorkArea *get_work_area() const + { + return workarea_; + } + TimelineMarkerList *get_markers() const + { + return markers_; + } + + virtual TimeRange get_video_cache_range() const override + { + return TimeRange(0, get_video_length()); + } + + virtual TimeRange get_audio_cache_range() const override + { + return TimeRange(0, get_audio_length()); + } + + std::vector get_enabled_streams_as_references() const; + + std::vector get_enabled_video_streams() const; + + std::vector get_enabled_audio_streams() const; + + virtual void retranslate() override; + + virtual Node *get_connected_texture_output(); + + virtual ValueHint get_connected_texture_value_hint(); + + virtual Node *get_connected_sample_output(); + + virtual ValueHint get_connected_sample_value_hint(); + + void set_waveform_enabled(bool e); + + bool is_video_auto_cache_enabled() const + { + fprintf(stderr, "sequence ac is a stub\n"); + return false; + } + void set_video_auto_cache_enabled(bool e) + { + (void) e; + fprintf(stderr, "sequence ac is a stub\n"); + } + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + const EncodingParams &get_last_used_encoding_params() const + { + return last_used_encoding_params_; + } + void set_last_used_encoding_params(const EncodingParams &p) + { + last_used_encoding_params_ = p; + } + + virtual bool load_custom(XmlStreamReader *reader, + SerializedData *data) override; + virtual void save_custom(XmlStreamWriter *writer) const override; + + static const std::string k_video_params_input; + static const std::string k_audio_params_input; + static const std::string k_subtitle_params_input; + + static const std::string k_texture_input; + static const std::string k_samples_input; + + static const SampleFormat k_default_sample_format; + + void verify_length(); + + void set_playhead(const Rational &t); + +protected: + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual Rational verify_length_internal(Track::Type type) const; + + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + + int add_stream(Track::Type type, const Variant &value); + int set_stream(Track::Type type, const Variant &value, int index); + +private: + Rational last_length_; + Rational video_length_; + Rational audio_length_; + + VideoParams cached_video_params_; + + AudioParams cached_audio_params_; + + TimelineWorkArea *workarea_; + TimelineMarkerList *markers_; + + bool autocache_input_video_; + bool autocache_input_audio_; + + EncodingParams last_used_encoding_params_; + + bool waveform_requests_enabled_; + + Rational playhead_; +}; + +} + +#endif // OAK_VIEWER_H diff --git a/src/node/src/param.cpp b/src/node/src/param.cpp new file mode 100644 index 000000000..4d3950f40 --- /dev/null +++ b/src/node/src/param.cpp @@ -0,0 +1,183 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "param.h" + +#include "node.h" + +namespace olive +{ + +std::string NodeInput::name() const +{ + if (is_valid()) { + return node_->get_input_name(input_); + } else { + return std::string(); + } +} + +bool NodeInput::is_hidden() const +{ + if (is_valid()) { + return node_->is_input_hidden(input_); + } else { + return false; + } +} + +bool NodeInput::is_connected() const +{ + if (is_valid()) { + return node_->is_input_connected(*this); + } else { + return false; + } +} + +bool NodeInput::is_keyframing() const +{ + if (is_valid()) { + return node_->is_input_keyframing(*this); + } else { + return false; + } +} + +bool NodeInput::is_array() const +{ + if (is_valid()) { + return node_->input_is_array(input_); + } else { + return false; + } +} + +InputFlags NodeInput::get_flags() const +{ + if (is_valid()) { + return node_->get_input_flags(input_); + } else { + return InputFlags(k_input_flag_normal); + } +} + +std::string NodeInput::get_input_name() const +{ + if (is_valid()) { + return node_->get_input_name(input_); + } else { + return std::string(); + } +} + +Node *NodeInput::get_connected_output() const +{ + if (is_valid()) { + return node_->get_connected_output(*this); + } else { + return nullptr; + } +} + +NodeValue::Type NodeInput::get_data_type() const +{ + if (is_valid()) { + return node_->get_input_data_type(input_); + } else { + return NodeValue::k_none; + } +} + +Variant NodeInput::get_default_value() const +{ + if (is_valid()) { + return node_->get_default_value(input_); + } else { + return Variant(); + } +} + +StringList NodeInput::get_combo_box_strings() const +{ + if (is_valid()) { + return node_->get_combo_box_strings(input_); + } else { + return StringList(); + } +} + +Variant NodeInput::get_property(const std::string &key) const +{ + if (is_valid()) { + return node_->get_input_property(input_, key); + } else { + return Variant(); + } +} + +std::map NodeInput::get_properties() const +{ + if (is_valid()) { + return node_->get_input_properties(input_); + } else { + return std::map(); + } +} + +Variant NodeInput::get_value_at_time(const Rational &time) const +{ + if (is_valid()) { + return node_->get_value_at_time(*this, time); + } else { + return Variant(); + } +} + +NodeKeyframe *NodeInput::get_keyframe_at_time_on_track(const Rational &time, + int track) const +{ + if (is_valid()) { + return node_->get_keyframe_at_time_on_track(*this, time, track); + } else { + return nullptr; + } +} + +Variant NodeInput::get_split_default_value_for_track(int track) const +{ + if (is_valid()) { + return node_->get_split_default_value_on_track(input_, track); + } else { + return Variant(); + } +} + +int NodeInput::get_array_size() const +{ + if (is_valid() && element_ == -1) { + return node_->input_array_size(input_); + } else { + return 0; + } +} + +} diff --git a/src/node/src/param.h b/src/node/src/param.h new file mode 100644 index 000000000..012430f2b --- /dev/null +++ b/src/node/src/param.h @@ -0,0 +1,365 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEPARAM_H +#define OAK_NODEPARAM_H + +#include +#include + +#include "value.h" + +namespace olive +{ + +class Node; +class NodeKeyframe; + +enum InputFlag : uint64_t { + /// By default, inputs are keyframable, connectable, and NOT arrays + k_input_flag_normal = 0x0, + k_input_flag_array = 0x1, + k_input_flag_not_keyframable = 0x2, + k_input_flag_not_connectable = 0x4, + k_input_flag_hidden = 0x8, + k_input_flag_ignore_invalidations = 0x10, + + k_input_flag_static = k_input_flag_not_keyframable | k_input_flag_not_connectable +}; + +class InputFlags { +public: + explicit InputFlags() + { + f_ = k_input_flag_normal; + } + + explicit InputFlags(uint64_t flags) + { + f_ = flags; + } + + InputFlags operator|(const InputFlags &f) const + { + InputFlags i = *this; + i |= f; + return i; + } + + InputFlags operator|(const InputFlag &f) const + { + InputFlags i = *this; + i |= f; + return i; + } + + InputFlags operator|(const uint64_t &f) const + { + InputFlags i = *this; + i |= f; + return i; + } + + InputFlags &operator|=(const InputFlags &f) + { + f_ |= f.f_; + return *this; + } + + InputFlags &operator|=(const InputFlag &f) + { + f_ |= f; + return *this; + } + + InputFlags &operator|=(const uint64_t &f) + { + f_ |= f; + return *this; + } + + InputFlags operator&(const InputFlags &f) const + { + InputFlags i = *this; + i &= f; + return i; + } + + InputFlags operator&(const InputFlag &f) const + { + InputFlags i = *this; + i &= f; + return i; + } + + InputFlags operator&(const uint64_t &f) const + { + InputFlags i = *this; + i &= f; + return i; + } + + InputFlags &operator&=(const InputFlags &f) + { + f_ &= f.f_; + return *this; + } + + InputFlags &operator&=(const InputFlag &f) + { + f_ &= f; + return *this; + } + + InputFlags &operator&=(const uint64_t &f) + { + f_ &= f; + return *this; + } + + InputFlags operator~() const + { + InputFlags i = *this; + i.f_ = ~i.f_; + return i; + } + + inline operator bool() const + { + return f_; + } + + inline const uint64_t &value() const + { + return f_; + } + +private: + uint64_t f_; +}; + +struct NodeInputPair { + bool operator==(const NodeInputPair &rhs) const + { + return node == rhs.node && input == rhs.input; + } + + Node *node; + std::string input; +}; + +/** + * @brief Defines a Node input + */ +class NodeInput { +public: + NodeInput() + { + node_ = nullptr; + element_ = -1; + } + + NodeInput(Node *n, const std::string &i, int e = -1) + { + node_ = n; + input_ = i; + element_ = e; + } + + bool operator==(const NodeInput &rhs) const + { + return node_ == rhs.node_ && input_ == rhs.input_ && + element_ == rhs.element_; + } + + bool operator!=(const NodeInput &rhs) const + { + return !(*this == rhs); + } + + bool operator<(const NodeInput &rhs) const + { + if (node_ != rhs.node_) { + return node_ < rhs.node_; + } + + if (input_ != rhs.input_) { + return input_ < rhs.input_; + } + + return element_ < rhs.element_; + } + + Node *node() const + { + return node_; + } + + NodeInputPair input_pair() const + { + return { node_, input_ }; + } + + const std::string &input() const + { + return input_; + } + + const int &element() const + { + return element_; + } + + void set_node(Node *node) + { + node_ = node; + } + + void set_input(const std::string &input) + { + input_ = input; + } + + void set_element(int e) + { + element_ = e; + } + + std::string name() const; + + bool is_valid() const + { + return node_ && !input_.empty() && element_ >= -1; + } + + bool is_hidden() const; + + bool is_connected() const; + + bool is_keyframing() const; + + bool is_array() const; + + InputFlags get_flags() const; + + std::string get_input_name() const; + + Node *get_connected_output() const; + + NodeValue::Type get_data_type() const; + + Variant get_default_value() const; + + StringList get_combo_box_strings() const; + + Variant get_property(const std::string &key) const; + std::map get_properties() const; + + Variant get_value_at_time(const Rational &time) const; + + NodeKeyframe *get_keyframe_at_time_on_track(const Rational &time, + int track) const; + + Variant get_split_default_value_for_track(int track) const; + + int get_array_size() const; + + void reset() + { + *this = NodeInput(); + } + +private: + Node *node_; + std::string input_; + int element_; +}; + +struct InputElementPair { + std::string input; + int element; + + bool operator<(const InputElementPair &rhs) const + { + if (input != rhs.input) { + return input < rhs.input; + } + + return element < rhs.element; + } + + bool operator==(const InputElementPair &rhs) const + { + return input == rhs.input && element == rhs.element; + } + + bool operator!=(const InputElementPair &rhs) const + { + return !(*this == rhs); + } +}; + +class NodeKeyframeTrackReference { +public: + NodeKeyframeTrackReference() + { + track_ = -1; + } + + NodeKeyframeTrackReference(const NodeInput &input, int track = 0) + { + input_ = input; + track_ = track; + } + + bool operator==(const NodeKeyframeTrackReference &rhs) const + { + return input_ == rhs.input_ && track_ == rhs.track_; + } + + const NodeInput &input() const + { + return input_; + } + + int track() const + { + return track_; + } + + bool is_valid() const + { + return input_.is_valid() && track_ >= 0; + } + + void reset() + { + *this = NodeKeyframeTrackReference(); + } + +private: + NodeInput input_; + int track_; +}; + +} + +#endif // OAK_NODEPARAM_H diff --git a/src/node/src/plugins/CMakeLists.txt b/src/node/src/plugins/CMakeLists.txt new file mode 100644 index 000000000..16f49fe94 --- /dev/null +++ b/src/node/src/plugins/CMakeLists.txt @@ -0,0 +1,22 @@ +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/plugins/plugin.h + node/plugins/plugin.cpp + PARENT_SCOPE +) diff --git a/src/node/src/plugins/plugin.cpp b/src/node/src/plugins/plugin.cpp new file mode 100644 index 000000000..1384084c5 --- /dev/null +++ b/src/node/src/plugins/plugin.cpp @@ -0,0 +1,683 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "plugin.h" + +#include "render/rendermanager.h" +#include "render/job/pluginjob.h" +#include "pluginSupport/oliveplugininstance.h" +#include "common/current.h" +#include "videoparams.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +std::map> + g_plugin_param_defaults; + +// De-Qt: QString::toLower() for ASCII keyword matching (keywords below are +// all ASCII, so ASCII case folding is equivalent here). +static std::string to_lower_ascii(std::string s) +{ + for (char &c : s) { + if (c >= 'A' && c <= 'Z') { + c = char(c - 'A' + 'a'); + } + } + return s; +} + +static bool contains(const std::string &haystack, const std::string &needle) +{ + return haystack.find(needle) != std::string::npos; +} + +static bool is_normalised_coord_system(const OFX::Host::Param::Base *param) +{ + return param->getDefaultCoordinateSystem() == + kOfxParamCoordinatesNormalised; +} + +static void get_project_extent(double &x_size, double &y_size) +{ + // oakcommon Current is type-erased; the facade installs a + // std::shared_ptr. Fall back to defaults when the + // slot is empty (worker process). + auto vps = std::static_pointer_cast( + Current::get_instance().current_video_params()); + static const olive::VideoParams fallback; + const olive::VideoParams &vp = vps ? *vps : fallback; + x_size = vp.width() * vp.pixel_aspect_ratio().to_double(); + y_size = vp.height(); +} + +static double to_canonical(double normalised, double extent) +{ + return extent > 0 ? normalised * extent : normalised; +} + +olive::Variant default_value_for_param(const OFX::Host::Param::Base *param) +{ + if (!param) { + return olive::Variant(); + } + const std::string &ofx_type = param->getType(); + const auto &props = param->getProperties(); + + if (ofx_type == kOfxParamTypeInteger || ofx_type == kOfxParamTypeChoice) { + return props.getIntProperty(kOfxParamPropDefault); + } + if (ofx_type == kOfxParamTypeBoolean) { + return props.getIntProperty(kOfxParamPropDefault) != 0; + } + if (ofx_type == kOfxParamTypeDouble) { + double val = props.getDoubleProperty(kOfxParamPropDefault); + if (is_normalised_coord_system(param)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + val = to_canonical(val, x_size); + } + return val; + } + if (ofx_type == kOfxParamTypeString || ofx_type == kOfxParamTypeStrChoice || + ofx_type == kOfxParamTypeCustom) { + return props.getStringProperty(kOfxParamPropDefault); + } + if (ofx_type == kOfxParamTypeRGB || ofx_type == kOfxParamTypeRGBA) { + const int count = (ofx_type == kOfxParamTypeRGBA) ? 4 : 3; + double values[4] = { 0.0, 0.0, 0.0, 1.0 }; + props.getDoublePropertyN(kOfxParamPropDefault, values, count); + const double alpha = (count == 4) ? values[3] : 1.0; + return olive::Variant::from_value( + olive::core::Color(values[0], values[1], values[2], alpha)); + } + if (ofx_type == kOfxParamTypeDouble2D || ofx_type == kOfxParamTypeDouble3D || + ofx_type == kOfxParamTypeInteger2D || + ofx_type == kOfxParamTypeInteger3D) { + const bool is_double = (ofx_type == kOfxParamTypeDouble2D || + ofx_type == kOfxParamTypeDouble3D); + const int count = (ofx_type == kOfxParamTypeDouble2D || + ofx_type == kOfxParamTypeInteger2D) ? + 2 : + 3; + if (is_double) { + double values[3] = { 0.0, 0.0, 0.0 }; + props.getDoublePropertyN(kOfxParamPropDefault, values, count); + if (is_normalised_coord_system(param)) { + double x_size, y_size; + get_project_extent(x_size, y_size); + values[0] = to_canonical(values[0], x_size); + values[1] = to_canonical(values[1], y_size); + if (count == 3) { + values[2] = to_canonical(values[2], x_size); + } + } + if (count == 2) { + return olive::Vector2D(values[0], values[1]); + } + return olive::Vector3D(values[0], values[1], values[2]); + } + int values[3] = { 0, 0, 0 }; + props.getIntPropertyN(kOfxParamPropDefault, values, count); + if (count == 2) { + return olive::Vector2D(values[0], values[1]); + } + return olive::Vector3D(values[0], values[1], values[2]); + } + if (ofx_type == kOfxParamTypeBytes) { + return olive::ByteArray(); + } + + return olive::Variant(); +} + +/** + * @brief Deduce whether an RGB/RGBA parameter semantically represents a color + * pickers or per-channel scalar values (e.g. gamma, contrast). + * + * Uses heuristics based on label, hint, display range, default values, + * and parent group name. + */ +std::string deduce_color_semantic( + const OFX::Host::Param::Base *param, + const std::map &group_labels) +{ + const std::string &ofx_type = param->getType(); + if (ofx_type != kOfxParamTypeRGB && ofx_type != kOfxParamTypeRGBA) { + return "color"; + } + + const std::string label = to_lower_ascii(param->getLabel()); + const std::string hint = to_lower_ascii(param->getHint()); + const std::string name = to_lower_ascii(param->getName()); + + // Rule 1: explicit color keywords → color + static const olive::StringList k_color_keywords = { "color", "colour", + "fill", "tint", + "key" }; + for (const std::string &kw : k_color_keywords) { + if (contains(label, kw) || contains(hint, kw) || contains(name, kw)) { + return "color"; + } + } + + // Rule 2: explicit scalar/adjustment keywords → scalar + static const olive::StringList k_scalar_keywords = { + "gamma", "contrast", "gain", "offset", + "saturation", "exposure", "brightness", "lift", + "multiply", "scale", "pivot" + }; + for (const std::string &kw : k_scalar_keywords) { + if (contains(label, kw) || contains(hint, kw) || contains(name, kw)) { + return "scalar"; + } + } + + // Rule 3: display range significantly outside/asymmetric to [0,1] → scalar + const auto &props = param->getProperties(); + const int dim = (ofx_type == kOfxParamTypeRGBA) ? 4 : 3; + double dmin[4] = { 0, 0, 0, 0 }; + double dmax[4] = { 1, 1, 1, 1 }; + props.getDoublePropertyN(kOfxParamPropDisplayMin, dmin, dim); + props.getDoublePropertyN(kOfxParamPropDisplayMax, dmax, dim); + bool range_looks_scalar = false; + for (int i = 0; i < dim; ++i) { + if (dmin[i] < -0.01 || dmax[i] > 1.01) { + range_looks_scalar = true; + break; + } + } + if (range_looks_scalar) { + return "scalar"; + } + + // Rule 4: default values all equal → scalar (lean) + double defs[4] = { 0, 0, 0, 1 }; + props.getDoublePropertyN(kOfxParamPropDefault, defs, dim); + bool all_equal = true; + for (int i = 1; i < dim; ++i) { + if (defs[i] != defs[0]) { + all_equal = false; + break; + } + } + if (all_equal) { + return "scalar"; + } + + // Rule 5: parent group contains scalar keywords → scalar + const std::string parent = to_lower_ascii(param->getParentName()); + if (!parent.empty()) { + for (const std::string &kw : k_scalar_keywords) { + if (contains(parent, kw)) { + return "scalar"; + } + } + } + + // Fallback + return "color"; +} + +std::map build_default_values( + const std::map ¶ms) +{ + std::map defaults; + for (const auto ¶m : params) { + const std::string &ofx_type = param.second->getType(); + if (ofx_type == kOfxParamTypeGroup || ofx_type == kOfxParamTypePage || + ofx_type == kOfxParamTypePushButton) { + continue; + } + const std::string input_id = param.second->getName(); + if (input_id.empty()) { + continue; + } + olive::Variant default_value = default_value_for_param(param.second); + if (default_value.is_null()) { + continue; + } + defaults[input_id] = default_value; + } + return defaults; +} +} +static std::string +clip_label_for_name(const std::string &name, + const OFX::Host::ImageEffect::ClipDescriptor *desc) +{ + if (name == kOfxImageEffectSimpleSourceClipName) { + return "Source"; + } + if (name == kOfxImageEffectTransitionSourceFromClipName) { + return "From"; + } + if (name == kOfxImageEffectTransitionSourceToClipName) { + return "To"; + } + + if (desc) { + const std::string ¶m_label = + desc->getProps().getStringProperty(kOfxPropLabel); + if (!param_label.empty()) { + return param_label; + } + } + + return name; +} + +olive::plugin::PluginNode::PluginNode(OFX::Host::ImageEffect::Instance *plugin) +{ + plugin_instance_ = plugin; + + const std::string &ctx = plugin_instance_->getContext(); + if (ctx == kOfxImageEffectContextFilter) { + sub_category_ = "Filter"; + } else if (ctx == kOfxImageEffectContextGenerator) { + sub_category_ = "Generator"; + } else if (ctx == kOfxImageEffectContextTransition) { + sub_category_ = "Transition"; + } else { + sub_category_ = "General"; + } + + bool has_texture_input = false; + std::map group_labels; + std::map page_labels; + std::map page_for_param; + + auto params = plugin_instance_->getParams(); + const std::string plugin_id = plugin_instance_->getPlugin()->getIdentifier(); + auto defaults_iter = g_plugin_param_defaults.find(plugin_id); + if (defaults_iter == g_plugin_param_defaults.end()) { + g_plugin_param_defaults.insert( + { plugin_id, build_default_values(params) }); + defaults_iter = g_plugin_param_defaults.find(plugin_id); + } + const std::map &defaults = defaults_iter->second; + for (auto param : params) { + const std::string &ofx_type = param.second->getType(); + if (ofx_type == kOfxParamTypeGroup) { + const std::string name = param.first; + const std::string label = param.second->getLabel(); + group_labels[name] = label.empty() ? name : label; + } else if (ofx_type == kOfxParamTypePage) { + const std::string name = param.first; + const std::string label = param.second->getLabel(); + page_labels[name] = label.empty() ? name : label; + + const auto &props = param.second->getProperties(); + int count = props.getDimension(kOfxParamPropPageChild); + for (int i = 0; i < count; ++i) { + const std::string &child = + props.getStringProperty(kOfxParamPropPageChild, i); + if (child == kOfxParamPageSkipRow || + child == kOfxParamPageSkipColumn) { + continue; + } + page_for_param[child] = page_labels.at(name); + } + } + } + + for (auto param : params) { + NodeValue::Type type = NodeValue::k_none; + + const std::string &ofx_type = param.second->getType(); + if (ofx_type == kOfxParamTypeInteger) { + type = NodeValue::k_int; + } else if (ofx_type == kOfxParamTypeDouble) { + type = NodeValue::k_float; + } else if (ofx_type == kOfxParamTypeBoolean) { + type = NodeValue::k_boolean; + } else if (ofx_type == kOfxParamTypeString) { + type = NodeValue::k_text; + } else if (ofx_type == kOfxParamTypeRGB || + ofx_type == kOfxParamTypeRGBA) { + type = NodeValue::k_color; + } else if (ofx_type == kOfxParamTypeChoice) { + type = NodeValue::k_combo; + } else if (ofx_type == kOfxParamTypeDouble2D || + ofx_type == kOfxParamTypeInteger2D) { + type = NodeValue::k_vec2; + } else if (ofx_type == kOfxParamTypeDouble3D || + ofx_type == kOfxParamTypeInteger3D) { + type = NodeValue::k_vec3; + } else if (ofx_type == kOfxParamTypeStrChoice) { + type = NodeValue::k_str_combo; + } else if (ofx_type == kOfxParamTypeBytes || + ofx_type == kOfxParamTypeCustom) { + type = NodeValue::k_binary; + } else if (ofx_type == kOfxParamTypePushButton) { + type = NodeValue::k_push_button; + } else if (ofx_type == kOfxParamTypeGroup || + ofx_type == kOfxParamTypePage) { + continue; + } else { + type = NodeValue::k_none; + } + + const std::string input_id = param.second->getName(); + if (input_id.empty()) { + continue; + } + const auto &props = param.second->getProperties(); + bool is_secret = props.getIntProperty(kOfxParamPropSecret) != 0; + if (type == NodeValue::k_none) { + continue; + } + Variant default_value; + auto default_it = defaults.find(input_id); + if (default_it != defaults.end()) { + default_value = default_it->second; + } + if (!default_value.is_null()) { + add_input(input_id, type, default_value); + if (type != NodeValue::k_push_button) { + set_standard_value(input_id, default_value); + } + } else { + add_input(input_id, type); + } + if (is_secret) { + set_input_flag(input_id, k_input_flag_hidden); + } + const std::string label = param.second->getLabel(); + if (!label.empty()) { + set_input_name(input_id, label); + } else { + set_input_name(input_id, input_id); + } + const std::string parent = param.second->getParentName(); + if (!parent.empty()) { + auto group_it = group_labels.find(parent); + set_input_property(input_id, "ui_group", + group_it != group_labels.end() ? group_it->second : + parent); + } + if (page_for_param.count(input_id)) { + set_input_property(input_id, "ui_page", + page_for_param.at(input_id)); + } + if (type == NodeValue::k_color) { + std::string semantic = + deduce_color_semantic(param.second, group_labels); + set_input_property(input_id, "color_semantic", semantic); + + const int dim = (ofx_type == kOfxParamTypeRGBA) ? 4 : 3; + double dmin[4] = { 0, 0, 0, 0 }; + double dmax[4] = { 1, 1, 1, 1 }; + props.getDoublePropertyN(kOfxParamPropDisplayMin, dmin, dim); + props.getDoublePropertyN(kOfxParamPropDisplayMax, dmax, dim); + set_input_property(input_id, "min", dmin[0]); + set_input_property(input_id, "max", dmax[0]); + + const std::string hint = param.second->getHint(); + if (!hint.empty()) { + set_input_property(input_id, "tooltip", hint); + } + } + if (type == NodeValue::k_combo || type == NodeValue::k_str_combo) { + StringList option_labels; + StringList option_values; + const int label_count = + props.getDimension(kOfxParamPropChoiceOption); + const int value_count = props.getDimension(kOfxParamPropChoiceEnum); + + for (int i = 0; i < label_count; ++i) { + const std::string &choice_label = + props.getStringProperty(kOfxParamPropChoiceOption, i); + option_labels.push_back(choice_label); + } + + for (int i = 0; i < value_count; ++i) { + const std::string &value = + props.getStringProperty(kOfxParamPropChoiceEnum, i); + option_values.push_back(value); + } + + if (option_labels.empty() && !option_values.empty()) { + option_labels = option_values; + } + if (option_values.empty() && !option_labels.empty()) { + option_values = option_labels; + } + + const int order_count = + props.getDimension(kOfxParamPropChoiceOrder); + if (order_count == int(option_labels.size()) && + option_labels.size() == option_values.size()) { + std::vector indices(option_labels.size()); + for (int i = 0; i < int(indices.size()); ++i) { + indices[i] = i; + } + + std::stable_sort( + indices.begin(), indices.end(), [&](int a, int b) { + return props.getIntProperty(kOfxParamPropChoiceOrder, + a) < + props.getIntProperty(kOfxParamPropChoiceOrder, + b); + }); + + StringList ordered_labels; + StringList ordered_values; + for (int index : indices) { + ordered_labels.push_back(option_labels[index]); + ordered_values.push_back(option_values[index]); + } + option_labels = ordered_labels; + option_values = ordered_values; + } + + if (!option_labels.empty()) { + set_combo_box_strings(input_id, option_labels); + if (type == NodeValue::k_str_combo) { + set_input_property(input_id, "combo_value_str", + option_values); + } + } + } + } + + const auto &clips = plugin_instance_->getDescriptor().getClips(); + for (const auto &entry : clips) { + if (entry.first == kOfxImageEffectOutputClipName) { + continue; + } + std::string input_id = entry.first; + add_input(input_id, NodeValue::k_texture); + set_input_name(input_id, clip_label_for_name(entry.first, entry.second)); + has_texture_input = true; + } + + const std::string source_id = kOfxImageEffectSimpleSourceClipName; + if (has_input_with_id(source_id)) { + set_effect_input(source_id); + } else if (has_input_with_id(k_texture_input)) { + set_effect_input(k_texture_input); + } else { + if (has_texture_input) { + add_input(k_texture_input, NodeValue::k_texture); + set_input_name(k_texture_input, "Texture"); + set_effect_input(k_texture_input); + } + } +} + +olive::plugin::PluginNode::~PluginNode() = default; +std::string olive::plugin::PluginNode::name() const +{ + const auto *plugin = plugin_instance_->getPlugin(); + return plugin->getDescriptor().getProps().getStringProperty(kOfxPropLabel); +} + +std::vector olive::plugin::PluginNode::category() const +{ + return { olive::Node::k_category_open_fx }; +} + +std::string olive::plugin::PluginNode::sub_category() const +{ + return sub_category_; +} + +std::string olive::plugin::PluginNode::description() const +{ + const auto *plugin = plugin_instance_->getPlugin(); + return plugin->getDescriptor().getProps().getStringProperty( + kOfxPropPluginDescription); +} +void olive::plugin::PluginNode::process_samples(const NodeValueRow &values, + const SampleBuffer &input, + SampleBuffer &output, + int index) const +{ + (void) values; + (void) index; + + if (!input.is_allocated() || input.channel_count() == 0 || + input.sample_count() == 0) { + if (output.is_allocated()) { + output.silence(); + } + return; + } + + if (!output.is_allocated() || + output.channel_count() != input.channel_count() || + output.sample_count() != input.sample_count()) { + output.set_audio_params(input.audio_params()); + output.set_sample_count(input.sample_count()); + output.allocate(); + } + + if (!output.is_allocated()) { + return; + } + + for (int channel = 0; channel < input.channel_count(); ++channel) { + output.fast_set(input, channel); + } +} + +void olive::plugin::PluginNode::generate_frame(FramePtr frame, + const GenerateJob &job) const +{ + (void) job; + + if (!frame) { + return; + } + + if (!frame->is_allocated()) { + frame->allocate(); + } + + if (!frame->is_allocated()) { + return; + } + + std::memset(frame->data(), 0, static_cast(frame->allocated_size())); +} +void olive::plugin::PluginNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + for (auto it = value.cbegin(); it != value.cend(); ++it) { + const NodeValue &input_value = it->second; + if (input_value.type() == NodeValue::k_texture || + input_value.type() == NodeValue::k_none) { + continue; + } + NodeValue tagged = input_value; + tagged.set_tag(it->first); + table->push(tagged); + } + + TexturePtr tex = nullptr; + const std::string source_key = kOfxImageEffectSimpleSourceClipName; + if (value.count(source_key)) { + tex = value.at(source_key).to_texture(); + } + if (!tex) { + auto tex_it = value.find(k_texture_input); + if (tex_it != value.cend()) { + tex = tex_it->second.to_texture(); + } + } + if (!tex) { + for (auto it = value.cbegin(); it != value.cend(); ++it) { + if (it->second.type() == NodeValue::k_texture) { + tex = it->second.to_texture(); + if (tex) { + break; + } + } + } + } + if (tex && plugin_instance_) { + PluginJob job(plugin_instance_, this, value, globals.time().in()); + + table->push(NodeValue::k_texture, tex->to_job(job), this); + } +} +void olive::plugin::PluginNode::push_button_clicked(std::string name) +{ + (void) name; +} + +std::string olive::plugin::PluginNode::id() const +{ + const auto *plugin = plugin_instance_->getPlugin(); + return plugin->getIdentifier(); +} + +olive::Node *olive::plugin::PluginNode::copy() const +{ + if (!plugin_instance_) { + return nullptr; + } + + const auto &contexts = plugin_instance_->getPlugin()->getContexts(); + std::string context = kOfxImageEffectContextFilter; + if (!contexts.empty() && + contexts.find(kOfxImageEffectContextFilter) == contexts.end()) { + context = *contexts.begin(); + } + + auto *instance = + plugin_instance_->getPlugin()->createInstance(context, nullptr); + if (!instance) { + return nullptr; + } + + auto *node = new PluginNode(instance); + if (auto *olive_instance = dynamic_cast(instance)) { + olive_instance->setNode( + std::shared_ptr(node, [](PluginNode *) {})); + } + return node; +} diff --git a/src/node/src/plugins/plugin.h b/src/node/src/plugins/plugin.h new file mode 100644 index 000000000..e17b42616 --- /dev/null +++ b/src/node/src/plugins/plugin.h @@ -0,0 +1,89 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_PLUGIN_NODES_H +#define OAK_PLUGIN_NODES_H +#include +#include + +#include "ofxhImageEffectAPI.h" +#include "ofxhPluginCache.h" +#include "ofxImageEffect.h" +#include "node.h" + +namespace olive +{ +namespace plugin +{ +const std::string k_texture_input = "tex_in"; + +class PluginNode : public olive::Node { +public: + PluginNode(OFX::Host::ImageEffect::Instance *plugin); + ~PluginNode() override; + + std::string name() const override; + std::string id() const override; + std::vector category() const override; + std::string sub_category() const override; + std::string description() const override; + + Node *copy() const override; + /** + * @brief The main processing function + * + * The node's main purpose is to take values from inputs to set values in outputs. For whatever subclass node you + * create, this is where the code for that goes. + * + * Note that as a video editor, the node graph has to work across time. Depending on the purpose of your node, it may + * output different values depending on the time, and even if not, it will likely be receiving different input + * depending on the time. Most of the difficult work here is handled by NodeInput::get_value() which you should pass + * the `time` parameter to. It will return its value (at that time, if it's keyframed), or pass the time to a + * corresponding output if it's connected to one. If your node doesn't directly deal with time, the default behavior + * of the NodeParam objects will handle everything related to it automatically. + */ + void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + /** + * @brief If Value() pushes a ShaderJob, this is the function that will process them. + */ + virtual void process_samples(const NodeValueRow &values, + const SampleBuffer &input, SampleBuffer &output, + int index) const override; + + /** + * @brief If Value() pushes a GenerateJob, override this function for the image to create + * + * @param frame + * + * The destination buffer. It will already be allocated and ready for writing to. + */ + virtual void generate_frame(FramePtr frame, const GenerateJob &job) const override; + +private: + std::string sub_category_; + +public: + void push_button_clicked(std::string name); +}; + +} +} + +#endif //PLUGIN_H diff --git a/src/node/src/project.cpp b/src/node/src/project.cpp new file mode 100644 index 000000000..103cd7a73 --- /dev/null +++ b/src/node/src/project.cpp @@ -0,0 +1,510 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "project.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "current.h" +#include "xmlutils.h" +#include "color/ociobase/ociobase.h" +#include "factory.h" +#include "group/group.h" +#include "project/folder/folder.h" +#include "serializeddata.h" +#include "pluginSupport/olivehost.h" +#include "ofxhPluginCache.h" +#include "render/diskmanager.h" + +namespace olive +{ + +const std::string Project::k_cache_location_setting_key = "cachesetting"; +const std::string Project::k_cache_path_key = "customcachepath"; +const std::string Project::k_color_config_filename = "colorconfigfilename"; +const std::string Project::k_default_input_color_space_key = + "defaultinputcolorspace"; +const std::string Project::k_color_reference_space = "colorreferencespace"; +const std::string Project::k_root_key = "root"; + +const std::string Project::k_item_mime_type = + "application/x-oliveprojectitemdata"; + +Project::Project() + : root_(nullptr) + , is_modified_(false) + , autorecovery_saved_(true) +{ + // Generate UUID for this project + regenerate_uuid(); + + // Initialize color manager (owned via unique_ptr; it used to be a + // QObject child of this Project) + color_manager_ = std::make_unique(this); + color_manager_->init(); +} + +Project::~Project() +{ + clear(); +} + +void Project::initialize() +{ + if (!root_) { + root_ = new Folder(); + add_node(root_); + root_->set_label("Root"); + settings_.insert_or_assign( + k_root_key, + std::to_string(reinterpret_cast(root_))); + } +} + +void Project::clear() +{ + is_being_cleared_ = true; + + // Notify each node while it is still fully constructed. Notifying from + // node destructors would walk into half-destroyed derived classes. + // (The former node_removed/removed_from_graph signals were removed with + // QObject; observers are notified through the facade layer instead.) + for (Node *node : node_children_) { + node->RemovedFromGraphEvent(this); + } + + // By deleting the last nodes first, we assume that nodes that are most important are deleted last + // (e.g. Project's ColorManager or ProjectSettingsNode. + for (auto it = node_children_.cbegin(); it != node_children_.cend(); it++) { + (*it)->set_caches_enabled(false); + } + + while (!node_children_.empty()) { + Node *node = node_children_.back(); + node_children_.pop_back(); + // Keep the parent pointer set while deleting: ~Node() calls + // disconnect_all(), whose edges point at nodes still owned by this + // graph, and the disconnect asserts require both sides to share the + // same parent. During teardown disconnect_all() runs silent (see + // is_being_cleared_) and ~Node() clears the parent itself. + delete node; + } + + // Reset root so initialize() can be called again after clear() + root_ = nullptr; + + is_being_cleared_ = false; +} + +SerializedData Project::load(XmlStreamReader *reader) +{ + SerializedData data; + std::set plugin_paths; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "uuid") { + this->set_uuid(reader->read_element_text()); + + } else if (reader->name() == "plugins") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "plugin") { + std::string bundle_path; + std::string file_path; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "bundle") { + bundle_path = attr.value; + } else if (attr.name == "file") { + file_path = attr.value; + } + } + + const std::string path = + bundle_path.empty() ? file_path : bundle_path; + if (!path.empty()) { + plugin_paths.insert(path); + } + + reader->skip_current_element(); + } else { + reader->skip_current_element(); + } + } + + if (!plugin_paths.empty()) { + if (!Current::get_instance().plugin_host() || + !Current::get_instance().plugin_cache()) { + // ADAPT(M9): plugin::load_plugins still takes a QString + // (pluginSupport/olivehost.h is converted in M9) + plugin::load_plugins(std::string()); + } + + auto *cache = OFX::Host::PluginCache::getPluginCache(); + for (const std::string &path : plugin_paths) { + cache->addFileToPath(path, true); + } + cache->scanPluginFiles(); + NodeFactory::register_plugin_nodes(); + } + + } else if (reader->name() == "nodes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + std::string id; + + { + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + } + + if (id.empty()) { + fprintf(stderr, "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + Node *node = NodeFactory::create_from_id(id); + + if (!node) { + fprintf(stderr, "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + // Disable cache while node is being loaded (we'll re-enable it later) + node->set_caches_enabled(false); + + node->load(reader, &data); + + add_node(node); + } + } + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "settings") { + while (xml_read_next_start_element(reader)) { + std::string key = reader->name(); + std::string val = reader->read_element_text(); + set_setting(key, val); + } + } else { + // Skip this + reader->skip_current_element(); + } + } + + // Resolve root if applicable + std::string root = get_setting(k_root_key); + if (!root.empty()) { + uintptr_t r = strtoull(root.c_str(), nullptr, 10); + auto it = data.node_ptrs.find(r); + if (it != data.node_ptrs.end()) { + Node *n = it->second; + assert(!root_); + root_ = dynamic_cast(n); + set_setting(k_root_key, + std::to_string(reinterpret_cast(root_))); + } + } + + return data; +} + +void Project::save(XmlStreamWriter *writer) const +{ + writer->write_attribute("version", std::to_string(1)); + + writer->write_text_element("uuid", this->get_uuid()); + + std::vector>> + plugins_to_save; + { + std::set seen; + for (Node *node : this->nodes()) { + auto *plugin = node->getPlugin(); + if (!plugin) { + continue; + } + + const std::string id = plugin->getIdentifier(); + const int major = plugin->getVersionMajor(); + const int minor = plugin->getVersionMinor(); + std::string bundle_path; + std::string file_path; + if (auto *binary = plugin->getBinary()) { + bundle_path = binary->getBundlePath(); + file_path = binary->getFilePath(); + } + + const std::string key = id + "|" + std::to_string(major) + "|" + + std::to_string(minor) + "|" + bundle_path + + "|" + file_path; + if (seen.count(key)) { + continue; + } + seen.insert(key); + + std::map attrs; + attrs.insert({ "id", id }); + attrs.insert({ "major", std::to_string(major) }); + attrs.insert({ "minor", std::to_string(minor) }); + if (!bundle_path.empty()) { + attrs.insert({ "bundle", bundle_path }); + } + if (!file_path.empty()) { + attrs.insert({ "file", file_path }); + } + + plugins_to_save.push_back({ id, attrs }); + } + } + + if (!plugins_to_save.empty()) { + writer->write_start_element("plugins"); + for (const auto &entry : plugins_to_save) { + writer->write_start_element("plugin"); + for (auto it = entry.second.cbegin(); it != entry.second.cend(); + ++it) { + writer->write_attribute(it->first, it->second); + } + writer->write_end_element(); + } + writer->write_end_element(); + } + + if (!this->nodes().empty()) { + writer->write_start_element("nodes"); + + for (Node *node : this->nodes()) { + writer->write_start_element("node"); + + node->save(writer); + + writer->write_end_element(); // node + } + + writer->write_end_element(); // nodes + } + + if (!this->settings_.empty()) { + writer->write_start_element("settings"); + + for (auto it = this->settings_.cbegin(); it != this->settings_.cend(); + it++) { + writer->write_text_element(it->first, it->second); + } + + writer->write_end_element(); // settings + } +} + +int Project::get_number_of_contexts_node_is_in(Node *node, bool except_itself) const +{ + int count = 0; + + for (Node *ctx : node_children_) { + if (ctx->context_contains_node(node) && (!except_itself || ctx != node)) { + count++; + } + } + + return count; +} + +void Project::add_node(Node *node) +{ + node_children_.push_back(node); + node->set_parent(this); + + // (The signal forwarding and node_added/added_to_graph emissions of the + // former ChildAdded branch were removed with QObject; the facade layer + // subscribes through oakengine_event instead.) + node->AddedToGraphEvent(this); +} + +void Project::remove_node(Node *node) +{ + auto it = std::find(node_children_.begin(), node_children_.end(), node); + if (it == node_children_.end()) { + return; + } + node_children_.erase(it); + + if (is_being_cleared_) { + // Teardown: everything is being destroyed anyway; clear() detaches + // and deletes the nodes itself. + return; + } + + node->RemovedFromGraphEvent(this); + + // Remove from any contexts + for (Node *context : node_children_) { + context->remove_node_from_context(node); + } + + node->set_parent(nullptr); +} + +std::string Project::name() const +{ + if (filename_.empty()) { + return "(untitled)"; + } else { + // Same as QFileInfo::completeBaseName(): filename up to the first dot + std::string base = std::filesystem::path(filename_).filename().string(); + std::string::size_type dot = base.find('.'); + if (dot != std::string::npos) { + base.erase(dot); + } + return base; + } +} + +const std::string &Project::filename() const +{ + return filename_; +} + +std::string Project::pretty_filename() const +{ + std::string fn = filename(); + + if (fn.empty()) { + return "(untitled)"; + } else { + return fn; + } +} + +void Project::set_filename(const std::string &s) +{ + filename_ = s; + + // (The name_changed() signal was removed with QObject; observers are + // notified through the facade layer.) +} + +void Project::set_modified(bool e) +{ + is_modified_ = e; + set_autorecovery_saved(!e); + + // (The modified_changed() signal was removed with QObject.) +} + +bool Project::has_autorecovery_been_saved() const +{ + return autorecovery_saved_; +} + +void Project::set_autorecovery_saved(bool e) +{ + autorecovery_saved_ = e; +} + +bool Project::is_new() const +{ + return !is_modified_ && filename_.empty(); +} + +std::string Project::get_cache_alongside_project_path() const +{ + if (!filename_.empty()) { + // Non-translated string so the path doesn't change if the language does + return (std::filesystem::path(filename_).parent_path() / "cache") + .string(); + } + return std::string(); +} + +std::string Project::cache_path() const +{ + CacheSetting setting = get_cache_location_setting(); + + switch (setting) { + case k_cache_use_default_location: + break; + case k_cache_custom_path: { + std::string cache_path = get_custom_cache_path(); + if (!cache_path.empty()) { + return cache_path; + } + break; + } + case k_cache_store_alongside_project: { + std::string alongside = get_cache_alongside_project_path(); + if (!alongside.empty()) { + return alongside; + } + break; + } + } + + // ADAPT(M7): render/diskmanager.h is converted in M7; it must return + // std::string here. + return DiskManager::instance()->get_default_cache_path(); +} + +void Project::regenerate_uuid() +{ + // Same text format as QUuid::createUuid().toString(): + // "{xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx}" (lowercase hex, version 4) + uint8_t b[16]; + std::random_device rd; + for (int i = 0; i < 16; i += 4) { + uint32_t r = (uint32_t(rd()) << 16) ^ uint32_t(rd()); + memcpy(b + i, &r, 4); + } + b[6] = (b[6] & 0x0F) | 0x40; // version 4 + b[8] = (b[8] & 0x3F) | 0x80; // variant 1 + + char text[40]; + snprintf(text, sizeof(text), + "{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x%02x%02x%02x%02x}", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + uuid_ = text; +} + +void Project::set_setting(const std::string &key, const std::string &value) +{ + settings_.insert_or_assign(key, value); + + // (The setting_changed() signal and the ColorManager change signals were + // removed with QObject; observers are notified through the facade layer.) + if (key == k_color_config_filename) { + color_manager_->update_config_from_filename(); + } +} + +} diff --git a/src/node/src/project.h b/src/node/src/project.h new file mode 100644 index 000000000..d6f1f56b3 --- /dev/null +++ b/src/node/src/project.h @@ -0,0 +1,271 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PROJECT_H +#define OAK_PROJECT_H + +#include +#include +#include +#include +#include + +#include "output/viewer/viewer.h" +#include "project/folder/folder.h" +#include "project/footage/footage.h" +#include "project/sequence/sequence.h" +#include "color/colormanager/colormanager.h" + +namespace olive +{ + +class NodeGroup; + +/** + * @brief A project instance containing all the data pertaining to the user's project + * + * A project instance uses a parent-child hierarchy of Item objects. Projects will usually contain the following: + * + * * Footage + * * Sequences + * * Folders + * * Project Settings + * * Window Layout + */ +class Project { +public: + enum CacheSetting { + k_cache_use_default_location, + k_cache_store_alongside_project, + k_cache_custom_path + }; + + Project(); + + virtual ~Project(); + + /** + * @brief Destructively destroys all nodes in the graph + */ + void clear(); + + /** + * @brief Retrieve a complete list of the nodes belonging to this graph + */ + const std::vector &nodes() const + { + return node_children_; + } + + void initialize(); + + SerializedData load(XmlStreamReader *reader); + void save(XmlStreamWriter *writer) const; + + int get_number_of_contexts_node_is_in(Node *node, + bool except_itself = false) const; + + std::string name() const; + + const std::string &filename() const; + std::string pretty_filename() const; + void set_filename(const std::string &s); + + Folder *root() const + { + return root_; + } + ColorManager *color_manager() const + { + return color_manager_.get(); + } + + bool is_modified() const + { + return is_modified_; + } + void set_modified(bool e); + + /** + * @brief True while clear()/teardown is deleting all nodes. + * + * Nodes use this to suppress cache invalidation storms while the graph + * is being torn down: invalidating during destruction walks edges into + * half-destroyed nodes and has repeatedly caused use-after-free crashes. + */ + bool is_being_cleared() const + { + return is_being_cleared_; + } + + bool has_autorecovery_been_saved() const; + void set_autorecovery_saved(bool e); + + bool is_new() const; + + std::string get_cache_alongside_project_path() const; + std::string cache_path() const; + + const std::string &get_uuid() const + { + return uuid_; + } + + void set_uuid(const std::string &uuid) + { + uuid_ = uuid; + } + + void regenerate_uuid(); + + /** + * @brief Returns the filename the project was saved as, but not necessarily where it is now + * + * May help for resolving relative paths. + */ + const std::string &get_saved_url() const + { + return saved_url_; + } + + void set_saved_url(const std::string &url) + { + saved_url_ = url; + } + + /** + * @brief Add a node to this graph (replaces QObject::setParent(project)) + * + * The project takes ownership of the node: it is deleted by clear() + * or by the project destructor. Use remove_node() to detach a node + * without deleting it. + */ + void add_node(Node *node); + + /** + * @brief Detach a node from this graph without deleting it + */ + void remove_node(Node *node); + + /** + * @brief Find project parent from object + * + * Nodes now store their graph directly (Node::project()), so this simply + * returns the graph the node belongs to. + */ + static Project *get_project_from_object(const Node *o) + { + return o ? o->project() : nullptr; + } + + static void copy_settings(Project *from, Project *to) + { + to->settings_ = from->settings_; + } + + static const std::string k_item_mime_type; + + static const std::string k_cache_location_setting_key; + static const std::string k_cache_path_key; + static const std::string k_color_config_filename; + static const std::string k_color_reference_space; + static const std::string k_default_input_color_space_key; + static const std::string k_root_key; + + std::string get_setting(const std::string &key) const + { + auto it = settings_.find(key); + return it != settings_.end() ? it->second : std::string(); + } + void set_setting(const std::string &key, const std::string &value); + + CacheSetting get_cache_location_setting() const + { + return static_cast( + atoi(get_setting(k_cache_location_setting_key).c_str())); + } + void set_cache_location_setting(CacheSetting s) + { + set_setting(k_cache_location_setting_key, std::to_string(s)); + } + + std::string get_custom_cache_path() const + { + return get_setting(k_cache_path_key); + } + void set_custom_cache_path(const std::string &path) + { + set_setting(k_cache_path_key, path); + } + + std::string get_color_config_filename() const + { + return get_setting(k_color_config_filename); + } + void set_color_config_filename(const std::string &s) + { + set_setting(k_color_config_filename, s); + } + + std::string get_default_input_color_space() const + { + return get_setting(k_default_input_color_space_key); + } + void set_default_input_color_space(const std::string &s) + { + set_setting(k_default_input_color_space_key, s); + } + + std::string get_color_reference_space() const + { + return get_setting(k_color_reference_space); + } + void set_color_reference_space(const std::string &s) + { + set_setting(k_color_reference_space, s); + } + +private: + std::string uuid_; + + Folder *root_; + + std::string filename_; + + std::string saved_url_; + + bool is_modified_; + + bool autorecovery_saved_; + + bool is_being_cleared_ = false; + + // Owned directly (ColorManager used to be a QObject child of this) + std::unique_ptr color_manager_; + + std::vector node_children_; + + std::map settings_; +}; + +} + +#endif // OAK_PROJECT_H diff --git a/src/node/src/project/CMakeLists.txt b/src/node/src/project/CMakeLists.txt new file mode 100644 index 000000000..4818d21e5 --- /dev/null +++ b/src/node/src/project/CMakeLists.txt @@ -0,0 +1,25 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(folder) +add_subdirectory(footage) +add_subdirectory(sequence) +add_subdirectory(serializer) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/project/folder/CMakeLists.txt b/src/node/src/project/folder/CMakeLists.txt new file mode 100644 index 000000000..d106632f4 --- /dev/null +++ b/src/node/src/project/folder/CMakeLists.txt @@ -0,0 +1,23 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/project/folder/folder.h + node/project/folder/folder.cpp + PARENT_SCOPE +) diff --git a/src/node/src/project/folder/folder.cpp b/src/node/src/project/folder/folder.cpp new file mode 100644 index 000000000..98ce060ee --- /dev/null +++ b/src/node/src/project/folder/folder.cpp @@ -0,0 +1,183 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "folder.h" + +#include + +#include "xmlutils.h" +#include "nodeundo.h" +#include "project/footage/footage.h" +#include "project/sequence/sequence.h" + +namespace olive +{ + +#define super Node + +const std::string Folder::k_child_input = "child_in"; + +Folder::Folder() +{ + set_flag(k_is_item); + + add_input(k_child_input, NodeValue::k_none, + InputFlags(k_input_flag_array | k_input_flag_not_keyframable)); +} + +Variant Folder::data(const DataType &d) const +{ + if (d == icon) { + return "folder"; + } + + return super::data(d); +} + +void Folder::retranslate() +{ + super::retranslate(); + + set_input_name(k_child_input, "Children"); +} + +Node *get_child_with_name_internal(const Folder *n, const std::string &s) +{ + for (int i = 0; i < n->item_child_count(); i++) { + Node *child = n->item_child(i); + + if (child->get_label() == s) { + return child; + } else if (Folder *subfolder = dynamic_cast(child)) { + if (Node *n2 = get_child_with_name_internal(subfolder, s)) { + return n2; + } + } + } + + return nullptr; +} + +Node *Folder::get_child_with_name(const std::string &s) const +{ + return get_child_with_name_internal(this, s); +} + +bool Folder::has_child_recursive(Node *child) const +{ + for (Node *i : item_children_) { + if (i == child) { + return true; + } else if (Folder *f = dynamic_cast(i)) { + if (f->has_child_recursive(child)) { + return true; + } + } + } + + return false; +} + +int Folder::index_of_child_in_array(Node *item) const +{ + int index_of_item = index_of_child(item); + + if (index_of_item == -1) { + return -1; + } + + return item_element_index_.at(index_of_item); +} + +void Folder::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + if (input == k_child_input && element != -1) { + Node *item = output; + + // The insert index is always our "count" because we only support appending in our internal + // model. For sorting/organizing, a QSortFilterProxyModel is used instead. + // (The begin_insert_item/end_insert_item signals were removed with + // QObject; observers are notified through the facade layer.) + item_children_.push_back(item); + item_element_index_.push_back(element); + item->set_folder(this); + } +} + +void Folder::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + if (input == k_child_input && element != -1) { + Node *item = output; + + int child_index = index_of_child(item); + // (The begin_remove_item/end_remove_item signals were removed with + // QObject.) + item_children_.erase(item_children_.begin() + child_index); + item_element_index_.erase(item_element_index_.begin() + child_index); + item->set_folder(nullptr); + } +} + +FolderAddChild::FolderAddChild(Folder *folder, Node *child) + : folder_(folder) + , child_(child) +{ +} + +void FolderAddChild::redo() +{ + int array_index = folder_->input_array_size(Folder::k_child_input); + folder_->input_array_append(Folder::k_child_input); + Node::connect_edge(child_, + NodeInput(folder_, Folder::k_child_input, array_index)); +} + +void FolderAddChild::undo() +{ + Node::disconnect_edge( + child_, NodeInput(folder_, Folder::k_child_input, + folder_->input_array_size(Folder::k_child_input) - 1)); + folder_->input_array_remove_last(Folder::k_child_input); +} + +void Folder::RemoveElementCommand::redo() +{ + if (!subcommand_) { + remove_index_ = folder_->index_of_child_in_array(child_); + if (remove_index_ != -1) { + NodeInput connected_input(folder_, Folder::k_child_input, + remove_index_); + subcommand_ = new MultiUndoCommand(); + subcommand_->add_child(new NodeEdgeRemoveCommand( + folder_->get_connected_output(connected_input), connected_input)); + subcommand_->add_child(new NodeArrayRemoveCommand( + folder_, Folder::k_child_input, remove_index_)); + } + } + + if (subcommand_) { + subcommand_->redo_now(); + } +} + +} diff --git a/src/node/src/project/folder/folder.h b/src/node/src/project/folder/folder.h new file mode 100644 index 000000000..ada8526ec --- /dev/null +++ b/src/node/src/project/folder/folder.h @@ -0,0 +1,211 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FOLDER_H +#define OAK_FOLDER_H + +#include "node.h" + +namespace olive +{ + +/** + * @brief The Folder class representing a directory in a project structure + * + * The Item base class already has support for children, but this functionality is disabled by default + * (see CanHaveChildren() override). The Folder is a specific type that enables this functionality. + */ +class Folder : public Node { +public: + Folder(); + + NODE_DEFAULT_FUNCTIONS(Folder) + + virtual std::string name() const override + { + return "Folder"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.folder"; + } + + virtual std::vector category() const override + { + return { k_category_project }; + } + + virtual std::string description() const override + { + return "Organize several items into a single collection."; + } + + virtual Variant data(const DataType &d) const override; + + virtual void retranslate() override; + + Node *get_child_with_name(const std::string &s) const; + bool child_exists_with_name(const std::string &s) const + { + return get_child_with_name(s); + } + + bool has_child_recursive(Node *child) const; + + int item_child_count() const + { + return item_children_.size(); + } + + Node *item_child(int i) const + { + return item_children_.at(i); + } + + const std::vector &children() const + { + return item_children_; + } + + int index_of_child(Node *item) const + { + auto it = std::find(item_children_.begin(), item_children_.end(), item); + return it == item_children_.end() ? -1 : + int(it - item_children_.begin()); + } + + int index_of_child_in_array(Node *item) const; + + template std::vector list_children_of_type() const + { + std::vector list; + + for (Node *node : item_children_) { + T *cast_test = dynamic_cast(node); + if (cast_test) { + list.push_back(cast_test); + } + + Folder *folder_test = dynamic_cast(node); + if (folder_test) { + std::vector sublist = + folder_test->list_children_of_type(); + list.insert(list.end(), sublist.begin(), sublist.end()); + } + } + + return list; + } + + static const std::string k_child_input; + + class RemoveElementCommand : public UndoCommand { + public: + RemoveElementCommand(Folder *folder, Node *child) + : folder_(folder) + , child_(child) + , subcommand_(nullptr) + { + } + + virtual ~RemoveElementCommand() override + { + delete subcommand_; + } + + protected: + virtual void redo() override; + + virtual void undo() override + { + if (subcommand_) { + subcommand_->undo_now(); + } + } + + private: + Folder *folder_; + + Node *child_; + + int remove_index_; + + MultiUndoCommand *subcommand_; + }; + +protected: + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + +private: + template + static void list_outputs_of_type_internal(const Folder *n, + std::vector &list, bool recursive) + { + for (const Node::OutputConnection &c : n->output_connections()) { + Node *connected = c.second.node(); + + T *cast_test = dynamic_cast(connected); + + if (cast_test) { + // Avoid duplicates + if (std::find(list.begin(), list.end(), cast_test) == + list.end()) { + list.push_back(cast_test); + } + } + + if (recursive) { + Folder *subfolder = dynamic_cast(connected); + + if (subfolder) { + ListOutputsOfTypeInternal(subfolder, list, recursive); + } + } + } + } + + std::vector item_children_; + std::vector item_element_index_; +}; + +class FolderAddChild : public UndoCommand { +public: + FolderAddChild(Folder *folder, Node *child); + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Folder *folder_; + + Node *child_; +}; + +} + +#endif // OAK_FOLDER_H diff --git a/src/node/src/project/footage/CMakeLists.txt b/src/node/src/project/footage/CMakeLists.txt new file mode 100644 index 000000000..579557064 --- /dev/null +++ b/src/node/src/project/footage/CMakeLists.txt @@ -0,0 +1,25 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/project/footage/footage.cpp + node/project/footage/footage.h + node/project/footage/footagedescription.cpp + node/project/footage/footagedescription.h + PARENT_SCOPE +) diff --git a/src/node/src/project/footage/footage.cpp b/src/node/src/project/footage/footage.cpp new file mode 100644 index 000000000..f7ce86fad --- /dev/null +++ b/src/node/src/project/footage/footage.cpp @@ -0,0 +1,1054 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "footage.h" + +#include +#include +#include +#include +#include +#include + +#include "codec/decoder.h" +#include "filefunctions.h" +#include "qtutils.h" +#include "xmlutils.h" +#include "config/config.h" +#include "olive/core/util/stringutils.h" +#include "color/colormanager/colormanager.h" +#include "project.h" +#include "render/job/footagejob.h" + +namespace olive +{ + +const std::string Footage::k_filename_input = "file_in"; + +#define super ViewerOutput + +// QDateTime::toMSecsSinceEpoch() equivalent for std::filesystem +static int64_t file_modification_time_ms(const std::string &path) +{ + std::error_code ec; + auto t = std::filesystem::last_write_time(path, ec); + if (ec) { + return 0; + } + + // Portable C++17 file-clock -> system-clock conversion + auto sys = std::chrono::time_point_cast( + t - decltype(t)::clock::now() + std::chrono::system_clock::now()); + return std::chrono::duration_cast( + sys.time_since_epoch()) + .count(); +} + +// Replaces QStandardPaths::CacheLocation for the footage metadata cache +static std::string get_footage_meta_cache_dir() +{ +#ifdef __APPLE__ + const char *home = getenv("HOME"); + if (home != nullptr && home[0] != '\0') { + return std::string(home) + "/Library/Caches/oak"; + } +#endif + return FileFunctions::get_configuration_location() + "/cache"; +} + +Footage::Footage(const std::string &filename) + : ViewerOutput(false, false) + , timestamp_(0) + , has_source_start_time_(false) + , proxy_enabled_(false) + , proxy_state_(ProxyManager::k_proxy_missing) + , proxy_video_stream_index_(-1) + , proxy_preset_version_(0) + , has_custom_proxy_params_(false) + , valid_(false) + , cancelled_(nullptr) + , total_stream_count_(0) +{ + set_flag(k_is_item); + + prepend_input(k_filename_input, NodeValue::k_file, + InputFlags(k_input_flag_not_connectable | + k_input_flag_not_keyframable)); + + clear(); + + if (!filename.empty()) { + set_filename(filename); + } + + // NOTE(de-Qt): the 5-second QTimer calling check_footage() and the + // AudioWaveformCache::validated -> connected_waveform_changed connection + // were removed with QObject; the facade layer must schedule + // check_footage() and forward cache notifications instead. +} + +void Footage::retranslate() +{ + super::retranslate(); + + set_input_name(k_filename_input, "Filename"); +} + +void Footage::InputValueChangedEvent(const std::string &input, int element) +{ + if (input == k_filename_input) { + // Reset internal stream cache + clear(); + + reprobe(); + } else { + super::InputValueChangedEvent(input, element); + } +} + +Rational Footage::verify_length_internal(Track::Type type) const +{ + if (type == Track::k_video) { + VideoParams first_stream = get_first_enabled_video_stream(); + + if (first_stream.is_valid()) { + return core::Timecode::timestamp_to_time(first_stream.duration(), + first_stream.time_base()); + } + } else if (type == Track::k_audio) { + AudioParams first_stream = get_first_enabled_audio_stream(); + + if (first_stream.is_valid()) { + return core::Timecode::timestamp_to_time(first_stream.duration(), + first_stream.time_base()); + } + } else if (type == Track::k_subtitle) { + SubtitleParams first_stream = get_first_enabled_subtitle_stream(); + + if (first_stream.is_valid()) { + return first_stream.duration(); + } + } + + return 0; +} + +std::string Footage::get_colorspace_to_use(const VideoParams ¶ms) const +{ + if (!params.colorspace().empty()) { + // The user explicitly set this stream's colorspace + return params.colorspace(); + } + + // No override: try auto-detecting from the media's color tags + const std::string detected = + project()->color_manager()->get_colorspace_for_ffmpeg_tags( + params.color_primaries(), params.color_transfer()); + if (!detected.empty()) { + return detected; + } + + return project()->color_manager()->get_default_input_color_space(); +} + +void Footage::clear() +{ + // Clear all dynamically created inputs + input_array_resize(k_video_params_input, 0); + input_array_resize(k_audio_params_input, 0); + input_array_resize(k_subtitle_params_input, 0); + + // Clear decoder link + decoder_.clear(); + + has_source_start_time_ = false; + source_start_time_ = Rational(); + source_start_time_source_.clear(); + clear_proxy(); + + // Clear total stream count + total_stream_count_ = 0; + + // Reset ready state + valid_ = false; +} + +void Footage::set_valid() +{ + valid_ = true; +} + +std::string Footage::filename() const +{ + return get_standard_value(k_filename_input).to_string(); +} + +void Footage::set_filename(const std::string &s) +{ + set_standard_value(k_filename_input, s); +} + +const int64_t &Footage::timestamp() const +{ + return timestamp_; +} + +void Footage::set_timestamp(const int64_t &t) +{ + timestamp_ = t; +} + +int Footage::get_stream_index(Track::Type type, int index) const +{ + switch (type) { + case Track::k_video: + if (index >= 0 && index < get_video_stream_count()) { + return get_video_params(index).stream_index(); + } + break; + case Track::k_audio: + if (index >= 0 && index < get_audio_stream_count()) { + return get_audio_params(index).stream_index(); + } + break; + case Track::k_subtitle: + if (index >= 0 && index < get_subtitle_stream_count()) { + return get_subtitle_params(index).stream_index(); + } + break; + case Track::k_none: + case Track::k_count: + break; + } + + return -1; +} + +Track::Reference Footage::get_reference_from_real_index(int real_index) const +{ + // Check video streams + for (int i = 0; i < get_video_stream_count(); i++) { + if (get_video_params(i).stream_index() == real_index) { + return Track::Reference(Track::k_video, i); + } + } + + for (int i = 0; i < get_audio_stream_count(); i++) { + if (get_audio_params(i).stream_index() == real_index) { + return Track::Reference(Track::k_audio, i); + } + } + + for (int i = 0; i < get_subtitle_stream_count(); i++) { + if (get_subtitle_params(i).stream_index() == real_index) { + return Track::Reference(Track::k_subtitle, i); + } + } + + return Track::Reference(); +} + +const std::string &Footage::decoder() const +{ + return decoder_; +} + +void Footage::set_source_start_time(const Rational &time, + const std::string &source) +{ + source_start_time_ = time; + source_start_time_source_ = source; + has_source_start_time_ = true; +} + +void Footage::clear_source_start_time() +{ + source_start_time_ = Rational(); + source_start_time_source_.clear(); + has_source_start_time_ = false; +} + +void Footage::set_proxy_enabled(bool enabled) +{ + if (proxy_enabled_ != enabled) { + fprintf(stderr, "Footage::set_proxy_enabled: %s %d\n", + filename().c_str(), enabled); + proxy_enabled_ = enabled; + if (Project *p = project()) { + p->set_modified(true); + } + // (The proxy_settings_changed() signal was removed with QObject.) + } +} + +void Footage::set_proxy(const std::string &path, ProxyManager::ProxyState state, + int video_stream_index, int preset_version, bool enabled) +{ + fprintf(stderr, "Footage::SetProxy: %s enabled=%d state=%s path=%s\n", + filename().c_str(), enabled, + ProxyManager::proxy_state_to_string(state).c_str(), path.c_str()); + proxy_path_ = path; + proxy_state_ = state; + proxy_video_stream_index_ = video_stream_index; + proxy_preset_version_ = preset_version; + proxy_enabled_ = enabled; + if (Project *p = project()) { + p->set_modified(true); + } + // (The proxy_settings_changed() signal was removed with QObject.) +} + +void Footage::clear_proxy() +{ + proxy_enabled_ = false; + proxy_path_.clear(); + proxy_state_ = ProxyManager::k_proxy_missing; + proxy_video_stream_index_ = -1; + proxy_preset_version_ = 0; + // (The proxy_settings_changed() signal was removed with QObject.) +} + +void Footage::set_custom_proxy_params(const ProxyManager::ProxyParams ¶ms) +{ + custom_proxy_params_ = params; + has_custom_proxy_params_ = true; + if (Project *p = project()) { + p->set_modified(true); + } + // (The proxy_settings_changed() signal was removed with QObject.) +} + +void Footage::clear_custom_proxy_params() +{ + if (has_custom_proxy_params_) { + has_custom_proxy_params_ = false; + custom_proxy_params_ = ProxyManager::ProxyParams(); + if (Project *p = project()) { + p->set_modified(true); + } + // (The proxy_settings_changed() signal was removed with QObject.) + } +} + +ProxyManager::ProxyParams Footage::get_effective_proxy_params() const +{ + if (has_custom_proxy_params_) { + return custom_proxy_params_; + } + + return ProxyManager::proxy_params_from_config(); +} + +std::string Footage::describe_video_stream(const VideoParams ¶ms) +{ + if (params.video_type() == VideoParams::k_video_type_still) { + return std::to_string(params.stream_index()) + ": Image - " + + std::to_string(params.width()) + "x" + + std::to_string(params.height()); + } else { + return std::to_string(params.stream_index()) + ": Video - " + + std::to_string(params.width()) + "x" + + std::to_string(params.height()); + } +} + +std::string Footage::describe_audio_stream(const AudioParams ¶ms) +{ + return std::to_string(params.stream_index()) + ": Audio - " + + std::to_string(params.channel_count()) + " Channel(s), " + + std::to_string(params.sample_rate()) + "Hz"; +} + +std::string Footage::describe_subtitle_stream(const SubtitleParams ¶ms) +{ + return std::to_string(params.stream_index()) + ": Subtitle"; +} + +void Footage::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + (void) globals; + + // Pop filename from table + std::string file = value.at(k_filename_input).to_string(); + + // Proxies can be globally disabled (Tools > Use Proxy Media) without + // losing each footage's individual proxy setting + // ADAPT(config 波次): engine config/config.h is still Qt-based; kept as-is + const bool proxies_allowed = + Config::current()[QStringLiteral("UseProxyMedia")].toBool(); + + // If the file exists and the reference is valid, push a footage job to the renderer + if (std::filesystem::exists(file)) { + // Push length + table->push(NodeValue::k_rational, Variant::from_value(get_length()), + this, "length"); + + // Push each stream as a footage job + for (int si = 0; si < get_total_stream_count(); si++) { + Track::Reference ref = get_reference_from_real_index(si); + FootageJob job(globals.time(), decoder_, filename(), ref.type(), + get_length(), globals.loop_mode()); + + if (ref.type() == Track::k_video) { + VideoParams vp = get_video_params(ref.index()); + + if (proxies_allowed && proxy_enabled_ && !proxy_path_.empty() && + proxy_video_stream_index_ == vp.stream_index() && + ProxyManager::get_proxy_state(proxy_path_) == + ProxyManager::k_proxy_ready) { + job.set_proxy(proxy_path_, "ffmpeg", 0); + } + + // Ensure the colorspace is valid and not empty + vp.set_colorspace(get_colorspace_to_use(vp)); + + // Adjust footage job's divider + if (globals.vparams().divider() > 1) { + // Use a divider appropriate for this target resolution + int calculated = VideoParams::get_divider_for_target_resolution( + vp.width(), vp.height(), + globals.vparams().effective_width(), + globals.vparams().effective_height()); + vp.set_divider( + std::min(calculated, globals.vparams().divider())); + } else { + // Render everything at full res + vp.set_divider(1); + } + + job.set_video_params(vp); + + table->push(NodeValue::k_texture, Texture::job(vp, job), this, + ref.to_string()); + } else if (ref.type() == Track::k_audio) { + AudioParams ap = get_audio_params(ref.index()); + job.set_audio_params(ap); + job.set_cache_path(project()->cache_path()); + + // Proxies generated with audio contain the video stream at + // index 0 followed by all source audio streams in source order + if (proxies_allowed && proxy_enabled_ && !proxy_path_.empty() && + ProxyManager::get_proxy_state(proxy_path_) == + ProxyManager::k_proxy_ready && + ProxyManager::proxy_filename_has_audio(proxy_path_)) { + int audio_rank = 0; + for (int sj = 0; sj < get_total_stream_count(); sj++) { + const Track::Reference other = + get_reference_from_real_index(sj); + + if (other.type() == Track::k_audio && + get_audio_params(other.index()).stream_index() < + ap.stream_index()) { + audio_rank++; + } + } + job.set_proxy(proxy_path_, "ffmpeg", audio_rank + 1); + } + + table->push(NodeValue::k_samples, Variant::from_value(job), this, + ref.to_string()); + } + } + } else if (!file.empty()) { + // Media is offline: push a generated warning frame for each video + // stream so missing media is clearly visible in the timeline instead + // of a transparent/black hole. generate_frame() draws the slat. + for (int si = 0; si < get_total_stream_count(); si++) { + Track::Reference ref = get_reference_from_real_index(si); + if (ref.type() != Track::k_video) { + continue; + } + + VideoParams vp = get_video_params(ref.index()); + if (!vp.is_valid()) { + vp = globals.vparams(); + } + vp.set_format(PixelFormat::u8); + vp.set_colorspace( + project()->color_manager()->get_default_input_color_space()); + + GenerateJob job(value); + table->push(NodeValue::k_texture, Texture::job(vp, job), this, + ref.to_string()); + } + } +} + +void Footage::generate_frame(FramePtr frame, const GenerateJob &job) const +{ + (void) job; + + // Dark red slat with diagonal stripes, matching the offline media + // warnings of other NLEs. NOTE(de-Qt): this used to be rasterized with + // QImage/QPainter including an antialiased "Media Offline" text overlay; + // the text overlay is gone (no font engine in oaknode) and the stripes + // are no longer antialiased. + uint8_t *data = reinterpret_cast(frame->data()); + const int w = frame->width(); + const int h = frame->height(); + const int linesize = frame->linesize_bytes(); + + const uint8_t bg_r = 60, bg_g = 0, bg_b = 0; + const uint8_t stripe_r = 120, stripe_g = 20, stripe_b = 20; + const int pen = std::max(2, h / 90); + const int stripe_step = std::max(16, h / 6); + + for (int y = 0; y < h; y++) { + uint8_t *row = data + y * linesize; + for (int x = 0; x < w; x++) { + // Diagonal stripes: same lines as the old QPainter loop + // (from (sx, 0) to (sx + h, h), i.e. x - y == const) + bool on_stripe = false; + int d = x - y; // ranges from -(h-1) to (w-1) + // Stripes start at x = -h and step by stripe_step + int rel = d + h; + if (rel >= 0) { + int m = rel % stripe_step; + if (m < pen) { + on_stripe = true; + } + } + + uint8_t *px = row + x * 4; + if (on_stripe) { + px[0] = stripe_r; + px[1] = stripe_g; + px[2] = stripe_b; + } else { + px[0] = bg_r; + px[1] = bg_g; + px[2] = bg_b; + } + px[3] = 255; + } + } +} + +std::string Footage::get_stream_type_name(Track::Type type) +{ + switch (type) { + case Track::k_video: + return "Video"; + case Track::k_audio: + return "Audio"; + case Track::k_subtitle: + return "Subtitle"; + case Track::k_none: + case Track::k_count: + break; + } + + return "Unknown"; +} + +Node *Footage::get_connected_texture_output() +{ + if (get_video_stream_count() > 0) { + return this; + } else { + return nullptr; + } +} + +Node *Footage::get_connected_sample_output() +{ + if (get_audio_stream_count() > 0) { + return this; + } else { + return nullptr; + } +} + +bool time_is_out_of_bounds(const Rational &time, const Rational &length) +{ + return time < 0 || time >= length; +} + +Rational Footage::adjust_time_by_loop_mode(Rational time, LoopMode loop_mode, + const Rational &length, + VideoParams::Type type, + const Rational &timebase) +{ + if (type == VideoParams::k_video_type_still) { + // No looping for still images + return 0; + } + + if (time_is_out_of_bounds(time, length)) { + switch (loop_mode) { + case LoopMode::k_loop_mode_off: + // Return no time to indicate no frame should be shown here + time = Rational::na_n; + break; + case LoopMode::k_loop_mode_clamp: + if (length < timebase) { + // No full frame fits in the range, so there is nothing to clamp to + time = Rational::na_n; + } else { + // Clamp footage time to length + time = std::clamp(time, Rational(0), length - timebase); + } + break; + case LoopMode::k_loop_mode_loop: + if (length <= 0) { + // Cannot loop around an empty range + time = Rational::na_n; + } else { + // Loop footage time around job length + do { + if (time >= length) { + time -= length; + } else { + time += length; + } + } while (time_is_out_of_bounds(time, length)); + } + break; + } + } + + return time; +} + +Variant Footage::data(const DataType &d) const +{ + switch (d) { + case created_time: { + std::error_code ec; + if (std::filesystem::exists(filename(), ec)) { + auto tp = QtUtils::get_creation_date( + std::filesystem::path(filename())); + return Variant(int64_t( + std::chrono::system_clock::to_time_t(tp))); + } + break; + } + case modified_time: { + std::error_code ec; + if (std::filesystem::exists(filename(), ec)) { + return Variant(int64_t(file_modification_time_ms(filename()) / + 1000)); + } + break; + } + case icon: { + if (valid_ && get_total_stream_count()) { + // Prioritize video > audio > image + VideoParams s = get_first_enabled_video_stream(); + + if (s.is_valid() && + s.video_type() != VideoParams::k_video_type_still) { + return "video"; + } else if (has_enabled_audio_streams()) { + return "audio"; + } else if (s.is_valid() && + s.video_type() == VideoParams::k_video_type_still) { + return "image"; + } else if (has_enabled_subtitle_streams()) { + return "subtitles"; + } + } + + return "error"; + } + case tooltip: { + if (valid_) { + std::string tip = "Filename: " + filename(); + + int vp_sz = get_video_stream_count(); + for (int i = 0; i < vp_sz; i++) { + VideoParams p = get_video_params(i); + + if (p.enabled()) { + tip.append("\n"); + tip.append(describe_video_stream(p)); + } + } + + int ap_sz = get_audio_stream_count(); + for (int i = 0; i < ap_sz; i++) { + AudioParams p = get_audio_params(i); + + if (p.enabled()) { + tip.append("\n"); + tip.append(describe_audio_stream(p)); + } + } + + int sp_sz = get_subtitle_stream_count(); + for (int i = 0; i < sp_sz; i++) { + SubtitleParams p = get_subtitle_params(i); + + if (p.enabled()) { + tip.append("\n"); + tip.append(describe_subtitle_stream(p)); + } + } + + return tip; + } else { + return "Invalid"; + } + } + default: + break; + } + + return super::data(d); +} + +bool Footage::load_custom(XmlStreamReader *reader, SerializedData *data) +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "timestamp") { + this->set_timestamp(strtoll(reader->read_element_text().c_str(), + nullptr, 10)); + } else if (reader->name() == "proxy") { + bool enabled = false; + ProxyManager::ProxyState state = ProxyManager::k_proxy_missing; + int stream = -1; + int preset_version = 0; + bool has_custom_params = false; + ProxyManager::ProxyParams custom_params; + { + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "enabled") { + enabled = + (attr.value == "1" || attr.value == "true"); + } else if (attr.name == "state") { + state = ProxyManager::proxy_state_from_string( + attr.value); + } else if (attr.name == "stream") { + stream = atoi(attr.value.c_str()); + } else if (attr.name == "preset") { + preset_version = atoi(attr.value.c_str()); + } else if (attr.name == "custom") { + has_custom_params = + (attr.value == "1" || attr.value == "true"); + } else if (attr.name == "pwidth") { + custom_params.width = atoi(attr.value.c_str()); + } else if (attr.name == "pheight") { + custom_params.height = atoi(attr.value.c_str()); + } else if (attr.name == "pdivider") { + custom_params.divider = atoi(attr.value.c_str()); + } else if (attr.name == "pcrf") { + custom_params.crf = atoi(attr.value.c_str()); + } else if (attr.name == "ppreset") { + custom_params.preset = attr.value; + } else if (attr.name == "pext") { + custom_params.extension = attr.value; + } else if (attr.name == "paudio") { + custom_params.include_audio = + (attr.value == "1" || attr.value == "true"); + } + } + } + + if (has_custom_params) { + set_custom_proxy_params(custom_params); + } + + const std::string path = reader->read_element_text(); + if (!path.empty()) { + set_proxy(path, state, stream, preset_version, enabled); + } else if (enabled) { + set_proxy_enabled(true); + } + } else if (reader->name() == "sourcestarttime") { + std::string source; + { + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "source") { + source = attr.value; + } + } + } + + const std::vector split = + core::StringUtils::split(reader->read_element_text(), '/'); + if (split.size() == 2) { + // Same ok-semantics as QString::toInt(&ok) + char *end = nullptr; + const int numerator = + strtol(split.at(0).c_str(), &end, 10); + bool numerator_ok = + end != split.at(0).c_str() && *end == '\0'; + const int denominator = + strtol(split.at(1).c_str(), &end, 10); + bool denominator_ok = + end != split.at(1).c_str() && *end == '\0'; + if (numerator_ok && denominator_ok && denominator) { + set_source_start_time(Rational(numerator, denominator), + source); + } + } + } else if (reader->name() == "viewer") { + if (!ViewerOutput::load_custom(reader, data)) { + return false; + } + } else { + reader->skip_current_element(); + } + } + + // The cached lengths are not serialized. Recompute them from the stream + // parameters that were just loaded so that worker processes and any code + // that reads GetLength() before InvalidateCache() runs sees valid values. + verify_length(); + + return true; +} + +void Footage::save_custom(XmlStreamWriter *writer) const +{ + writer->write_text_element("timestamp", std::to_string(this->timestamp())); + + if (!proxy_path_.empty() || proxy_enabled_) { + writer->write_start_element("proxy"); + writer->write_attribute("enabled", proxy_enabled_ ? "1" : "0"); + writer->write_attribute( + "state", ProxyManager::proxy_state_to_string(proxy_state_)); + writer->write_attribute("stream", + std::to_string(proxy_video_stream_index_)); + writer->write_attribute("preset", + std::to_string(proxy_preset_version_)); + if (has_custom_proxy_params_) { + writer->write_attribute("custom", "1"); + writer->write_attribute("pwidth", + std::to_string(custom_proxy_params_.width)); + writer->write_attribute( + "pheight", std::to_string(custom_proxy_params_.height)); + writer->write_attribute( + "pdivider", std::to_string(custom_proxy_params_.divider)); + writer->write_attribute("pcrf", + std::to_string(custom_proxy_params_.crf)); + writer->write_attribute("ppreset", custom_proxy_params_.preset); + writer->write_attribute("pext", custom_proxy_params_.extension); + writer->write_attribute( + "paudio", custom_proxy_params_.include_audio ? "1" : "0"); + } + writer->write_characters(proxy_path_); + writer->write_end_element(); + } + + if (has_source_start_time_) { + writer->write_start_element("sourcestarttime"); + writer->write_attribute("source", source_start_time_source_); + writer->write_characters( + std::to_string(source_start_time_.numerator()) + "/" + + std::to_string(source_start_time_.denominator())); + writer->write_end_element(); + } + + writer->write_start_element("viewer"); + + ViewerOutput::save_custom(writer); + + writer->write_end_element(); // viewer +} + +void Footage::reprobe() +{ + // Determine if file still exists + std::string filename = this->filename(); + + // In case of failure to load file, set timestamp to a value that will always be invalid so we + // continuously reprobe + set_timestamp(0); + + if (!filename.empty()) { + std::error_code ec; + if (std::filesystem::exists(filename, ec)) { + // Grab timestamp + set_timestamp(file_modification_time_ms(filename)); + + // Determine if we've already cached the metadata of this file + std::string cache_dir = get_footage_meta_cache_dir(); + std::filesystem::create_directories(cache_dir, ec); + std::string meta_cache_file = + (std::filesystem::path(cache_dir) / + FileFunctions::get_unique_file_identifier(filename)) + .string(); + + FootageDescription footage_info; + + // Try to load footage info from cache + if (!std::filesystem::exists(meta_cache_file, ec) || + !footage_info.load(meta_cache_file)) { + // Probe and create cache + std::vector decoder_list = + Decoder::receive_list_of_all_decoders(); + + for (DecoderPtr decoder : decoder_list) { + footage_info = decoder->probe(filename, cancelled_); + + if (footage_info.is_valid()) { + break; + } + } + + if (!cancelled_ || !cancelled_->heard_cancel()) { + // Only cache successful probes; caching a failed probe + // would make every future load re-use the invalid metadata + if (footage_info.is_valid() && + !footage_info.save(meta_cache_file)) { + fprintf(stderr, + "Failed to save stream cache, footage will " + "have to be re-probed\n"); + } + } + } + + if (footage_info.is_valid()) { + decoder_ = footage_info.decoder(); + + input_array_resize(k_video_params_input, + int(footage_info.get_video_streams().size())); + for (int i = 0; i < int(footage_info.get_video_streams().size()); + i++) { + VideoParams video_stream = + footage_info.get_video_streams().at(i); + + if (i < input_array_size(k_video_params_input)) { + VideoParams existing = this->get_video_params(i); + if (existing.is_valid()) { + video_stream = + merge_video_stream(video_stream, existing); + } + } + + set_stream(Track::k_video, Variant::from_value(video_stream), + i); + } + + input_array_resize(k_audio_params_input, + int(footage_info.get_audio_streams().size())); + for (int i = 0; i < int(footage_info.get_audio_streams().size()); + i++) { + set_stream(Track::k_audio, + Variant::from_value( + footage_info.get_audio_streams().at(i)), + i); + } + + input_array_resize( + k_subtitle_params_input, + int(footage_info.get_subtitle_streams().size())); + for (int i = 0; + i < int(footage_info.get_subtitle_streams().size()); i++) { + set_stream(Track::k_subtitle, + Variant::from_value( + footage_info.get_subtitle_streams().at(i)), + i); + } + + total_stream_count_ = footage_info.get_stream_count(); + if (footage_info.has_source_start_time()) { + set_source_start_time(footage_info.source_start_time(), + footage_info.source_start_time_source()); + } + + set_valid(); + } + } + } +} + +VideoParams Footage::merge_video_stream(const VideoParams &base, + const VideoParams &over) +{ + VideoParams merged = base; + + merged.set_pixel_aspect_ratio(over.pixel_aspect_ratio()); + merged.set_interlacing(over.interlacing()); + merged.set_colorspace(over.colorspace()); + merged.set_premultiplied_alpha(over.premultiplied_alpha()); + merged.set_video_type(over.video_type()); + merged.set_color_range(over.color_range()); + if (merged.video_type() == VideoParams::k_video_type_image_sequence) { + merged.set_start_time(over.start_time()); + merged.set_duration(over.duration()); + merged.set_frame_rate(over.frame_rate()); + merged.set_time_base(over.time_base()); + } + + return merged; +} + +void Footage::check_footage() +{ + // NOTE(de-Qt): the qApp->activeWindow() gate moved to the app layer; the + // caller (facade) decides when it is appropriate to check. + std::string fn = filename(); + + if (!fn.empty()) { + int64_t current_file_timestamp = file_modification_time_ms(fn); + + if (current_file_timestamp != timestamp()) { + // File has changed! + clear(); + reprobe(); + invalidate_all(k_filename_input); + } + } +} + +void Footage::default_color_space_changed() +{ + bool inv = false; + int sz = get_video_stream_count(); + for (int i = 0; i < sz; i++) { + // Check if any of our streams are using the default colorspace + if (get_video_params(i).colorspace().empty()) { + inv = true; + break; + } + } + + if (inv) { + invalidate_all(k_video_params_input); + } +} + +void Footage::proxy_ready(const std::string &source_filename, int stream_index, + const std::string &proxy_filename) +{ + proxy_finished(source_filename, stream_index, proxy_filename, + ProxyManager::k_proxy_ready); +} + +void Footage::proxy_finished(const std::string &source_filename, + int stream_index, + const std::string &proxy_filename, + ProxyManager::ProxyState state) +{ + if (filename() != source_filename || + proxy_video_stream_index_ != stream_index || + proxy_path_ != proxy_filename) { + return; + } + + proxy_state_ = state; + invalidate_all(k_filename_input); +} + +} diff --git a/src/node/src/project/footage/footage.h b/src/node/src/project/footage/footage.h new file mode 100644 index 000000000..7b3d1db4a --- /dev/null +++ b/src/node/src/project/footage/footage.h @@ -0,0 +1,359 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FOOTAGE_H +#define OAK_FOOTAGE_H + +#include +#include +#include + +#include + +#include "codec/decoder.h" +#include "codec/proxymanager.h" +#include "footagedescription.h" +#include "output/viewer/viewer.h" +#include "render/cancelatom.h" +#include "videoparams.h" + +namespace olive +{ + +/** + * @brief A reference to an external media file with metadata in a project structure + * + * Footage objects serve two purposes: storing metadata about external media and storing it as a project item. + * Footage objects store a list of Stream objects which store the majority of video/audio metadata. These streams + * are identical to the stream data in the files. + */ +class Footage : public ViewerOutput { +public: + /** + * @brief Footage Constructor + */ + Footage(const std::string &filename = std::string()); + + NODE_DEFAULT_FUNCTIONS(Footage) + + virtual std::string name() const override + { + return "Media"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.footage"; + } + + virtual std::vector category() const override + { + return { k_category_project }; + } + + virtual std::string description() const override + { + return "Import video, audio, or still image files into the " + "composition."; + } + + virtual void retranslate() override; + + /** + * @brief Reset Footage state ready for running through Probe() again + * + * If a Footage object needs to be re-probed (e.g. source file changes or Footage is linked to a new file), its + * state needs to be reset so the Decoder::Probe() function can accurately mirror the source file. Clear() will + * reset the Footage object's state to being freshly created (keeping the filename). + * + * In most cases, you'll be using olive::ProbeMedia() for re-probing which already runs Clear(), so you won't need + * to worry about this. + */ + void clear(); + + bool is_valid() const + { + return valid_; + } + + /** + * @brief Sets this footage to valid and ready to use + */ + void set_valid(); + + /** + * @brief Return the current filename of this Footage object + */ + std::string filename() const; + + /** + * @brief Set the filename + * + * NOTE: This does not automtaically clear the old streams and re-probe for new ones. If the file link has been + * changed, this will need to be done manually. + * + * @param s + * + * New filename + */ + void set_filename(const std::string &s); + + /** + * @brief Retrieve the last modified time/date + * + * The file's last modified timestamp is stored for potential organization in the ProjectExplorer. It can be + * retrieved here. + */ + const int64_t ×tamp() const; + + /** + * @brief Set the last modified time/date + * + * This should probably only be done on import or replace. + * + * @param t + * + * New last modified time/date + */ + void set_timestamp(const int64_t &t); + + void set_cancel_pointer(CancelAtom *c) + { + cancelled_ = c; + } + + int get_stream_index(Track::Type type, int index) const; + int get_stream_index(const Track::Reference &ref) const + { + return get_stream_index(ref.type(), ref.index()); + } + + Track::Reference get_reference_from_real_index(int real_index) const; + + /** + * @brief Get the Decoder ID set when this Footage was probed + * + * @return + * + * A decoder ID + */ + const std::string &decoder() const; + + bool has_source_start_time() const + { + return has_source_start_time_; + } + + const Rational &source_start_time() const + { + return source_start_time_; + } + + const std::string &source_start_time_source() const + { + return source_start_time_source_; + } + + void set_source_start_time(const Rational &time, const std::string &source); + + /** + * @brief Removes any source start time (auto-detected or manual) + */ + void clear_source_start_time(); + + bool proxy_enabled() const + { + return proxy_enabled_; + } + + void set_proxy_enabled(bool enabled); + + const std::string &proxy_path() const + { + return proxy_path_; + } + + int proxy_video_stream_index() const + { + return proxy_video_stream_index_; + } + + int proxy_preset_version() const + { + return proxy_preset_version_; + } + + ProxyManager::ProxyState proxy_state() const + { + return proxy_state_; + } + + void set_proxy(const std::string &path, ProxyManager::ProxyState state, + int video_stream_index, int preset_version, bool enabled); + + void clear_proxy(); + + /** + * @brief Returns true if this footage uses its own proxy parameters + * instead of the global proxy settings + */ + bool has_custom_proxy_params() const + { + return has_custom_proxy_params_; + } + + const ProxyManager::ProxyParams &custom_proxy_params() const + { + return custom_proxy_params_; + } + + /** + * @brief Sets per-footage proxy parameters, overriding the global settings + */ + void set_custom_proxy_params(const ProxyManager::ProxyParams ¶ms); + + /** + * @brief Reverts this footage to using the global proxy settings + */ + void clear_custom_proxy_params(); + + /** + * @brief Returns the custom proxy parameters if set, otherwise the + * parameters from the global application config + */ + ProxyManager::ProxyParams get_effective_proxy_params() const; + + static std::string describe_video_stream(const VideoParams ¶ms); + static std::string describe_audio_stream(const AudioParams ¶ms); + static std::string describe_subtitle_stream(const SubtitleParams ¶ms); + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + virtual void generate_frame(FramePtr frame, + const GenerateJob &job) const override; + + static std::string get_stream_type_name(Track::Type type); + + virtual Node *get_connected_texture_output() override; + + virtual Node *get_connected_sample_output() override; + + static Rational adjust_time_by_loop_mode(Rational time, LoopMode loop_mode, + const Rational &length, + VideoParams::Type type, + const Rational &timebase); + + virtual Variant data(const DataType &d) const override; + + virtual int get_total_stream_count() const override + { + return total_stream_count_; + } + + virtual bool load_custom(XmlStreamReader *reader, + SerializedData *data) override; + virtual void save_custom(XmlStreamWriter *writer) const override; + + /** + * @brief Re-probe if the file on disk changed + * + * Formerly a private slot on a 5-second QTimer (and gated on + * qApp->activeWindow()). The timer and the window check live in the app + * layer now; the facade must call this periodically. + */ + void check_footage(); + + /** + * @brief Formerly a slot connected to + * ColorManager::default_input_changed; must now be called explicitly by + * whoever owns that notification (facade layer). + */ + void default_color_space_changed(); + + /** + * @brief Formerly slots connected to ProxyManager::proxy_ready / + * proxy_finished; must now be called explicitly by whoever owns those + * notifications (facade layer / proxy manager de-Qt wave). + */ + void proxy_ready(const std::string &source_filename, int stream_index, + const std::string &proxy_filename); + void proxy_finished(const std::string &source_filename, int stream_index, + const std::string &proxy_filename, + ProxyManager::ProxyState state); + +public: + static const std::string k_filename_input; + +protected: + virtual void InputValueChangedEvent(const std::string &input, + int element) override; + + virtual Rational verify_length_internal(Track::Type type) const override; + +private: + std::string get_colorspace_to_use(const VideoParams ¶ms) const; + + void reprobe(); + + VideoParams merge_video_stream(const VideoParams &base, + const VideoParams &over); + + /** + * @brief Internal timestamp object + */ + int64_t timestamp_; + + /** + * @brief Internal attached decoder ID + */ + std::string decoder_; + + Rational source_start_time_; + + std::string source_start_time_source_; + + bool has_source_start_time_; + + bool proxy_enabled_; + + std::string proxy_path_; + + ProxyManager::ProxyState proxy_state_; + + int proxy_video_stream_index_; + + int proxy_preset_version_; + + bool has_custom_proxy_params_; + + ProxyManager::ProxyParams custom_proxy_params_; + + bool valid_; + + CancelAtom *cancelled_; + + int total_stream_count_; +}; + +} + +#endif // OAK_FOOTAGE_H diff --git a/src/node/src/project/footage/footagedescription.cpp b/src/node/src/project/footage/footagedescription.cpp new file mode 100644 index 000000000..5579831eb --- /dev/null +++ b/src/node/src/project/footage/footagedescription.cpp @@ -0,0 +1,205 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "footagedescription.h" + +#include +#include +#include +#include + +#include "xmlutils.h" +#include "olive/core/util/stringutils.h" +#include "project/serializer/typeserializer.h" + +namespace olive +{ + +bool FootageDescription::load(const std::string &filename) +{ + // Reset self + *this = FootageDescription(); + + std::ifstream file(filename, std::ios::binary); + + if (file.is_open()) { + std::stringstream ss; + ss << file.rdbuf(); + XmlStreamReader reader(ss.str()); + + bool found_streamcache = false; + + while (xml_read_next_start_element(&reader)) { + if (reader.name() == "streamcache") { + found_streamcache = true; + // Default to first version of metadata (which wasn't versioned at all) + unsigned version = 1; + + { + for (const XmlStreamAttribute &attr : reader.attributes()) { + if (attr.name == "version") { + version = + strtoul(attr.value.c_str(), nullptr, 10); + } + } + } + + if (version != k_footage_meta_version) { + // If this is a different version, discard so we can probe new data + return false; + } + + while (xml_read_next_start_element(&reader)) { + if (reader.name() == "decoder") { + decoder_ = reader.read_element_text(); + } else if (reader.name() == "sourcestarttime") { + std::string source; + { + for (const XmlStreamAttribute &attr : + reader.attributes()) { + if (attr.name == "source") { + source = attr.value; + } + } + } + + const std::vector split = + core::StringUtils::split(reader.read_element_text(), + '/'); + if (split.size() == 2) { + set_source_start_time( + Rational(atoi(split.at(0).c_str()), + atoi(split.at(1).c_str())), + source); + } + } else if (reader.name() == "streams") { + { + for (const XmlStreamAttribute &attr : + reader.attributes()) { + if (attr.name == "count") { + total_stream_count_ = + atoi(attr.value.c_str()); + } + } + } + + while (xml_read_next_start_element(&reader)) { + if (reader.name() == "video") { + VideoParams vp; + vp.load(&reader); + add_video_stream(vp); + } else if (reader.name() == "audio") { + AudioParams ap = + TypeSerializer::load_audio_params(&reader); + add_audio_stream(ap); + } else if (reader.name() == "subtitle") { + SubtitleParams sp; + sp.load(&reader); + add_subtitle_stream(sp); + } else { + reader.skip_current_element(); + } + } + } else { + reader.skip_current_element(); + } + } + } else { + reader.skip_current_element(); + } + } + + file.close(); + + if (reader.has_error()) { + fprintf(stderr, "Failed to load footage description for %s: %s\n", + filename.c_str(), reader.error_string().c_str()); + } else { + // Only accept files whose root element is the one Save() writes + return found_streamcache; + } + } + + return false; +} + +bool FootageDescription::save(const std::string &filename) const +{ + XmlStreamWriter writer; + + writer.write_start_element("streamcache"); + + writer.write_attribute("version", std::to_string(k_footage_meta_version)); + + writer.write_text_element("decoder", decoder_); + + if (has_source_start_time_) { + writer.write_start_element("sourcestarttime"); + writer.write_attribute("source", source_start_time_source_); + writer.write_characters(std::to_string(source_start_time_.numerator()) + + "/" + + std::to_string(source_start_time_.denominator())); + writer.write_end_element(); + } + + writer.write_start_element("streams"); + + writer.write_attribute("count", std::to_string(total_stream_count_)); + + for (const VideoParams &vp : video_streams_) { + writer.write_start_element("video"); + vp.save(&writer); + writer.write_end_element(); // video + } + + for (const AudioParams &ap : audio_streams_) { + writer.write_start_element("audio"); + TypeSerializer::save_audio_params(&writer, ap); + writer.write_end_element(); // audio + } + + for (const SubtitleParams &sp : subtitle_streams_) { + writer.write_start_element("subtitle"); + sp.save(&writer); + writer.write_end_element(); // audio + } + + writer.write_end_element(); // streams + + writer.write_end_element(); // streamcache + + writer.write_end_document(); + + // NOTE(de-Qt): the former writeStartDocument() XML declaration is no + // longer emitted; the file is still valid XML and readable by both the + // old Qt reader and the new one. + std::ofstream file(filename, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + return false; + } + + file << writer.output(); + file.close(); + + return true; +} + +} diff --git a/src/node/src/project/footage/footagedescription.h b/src/node/src/project/footage/footagedescription.h new file mode 100644 index 000000000..baad4f43d --- /dev/null +++ b/src/node/src/project/footage/footagedescription.h @@ -0,0 +1,216 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FOOTAGEDESCRIPTION_H +#define OAK_FOOTAGEDESCRIPTION_H + +#include +#include +#include + +#include + +#include "output/track/track.h" +#include "subtitleparams.h" +#include "videoparams.h" + +namespace olive +{ + +class FootageDescription { +public: + FootageDescription(const std::string &decoder = std::string()) + : decoder_(decoder) + , total_stream_count_(0) + , has_source_start_time_(false) + { + } + + bool is_valid() const + { + return !decoder_.empty() && + (!video_streams_.empty() || !audio_streams_.empty() || + !subtitle_streams_.empty()); + } + + const std::string &decoder() const + { + return decoder_; + } + + void add_video_stream(const VideoParams &video_params) + { + assert(!has_stream_index(video_params.stream_index())); + + video_streams_.push_back(video_params); + } + + void add_audio_stream(const AudioParams &audio_params) + { + assert(!has_stream_index(audio_params.stream_index())); + + audio_streams_.push_back(audio_params); + } + + void add_subtitle_stream(const SubtitleParams &sub_params) + { + assert(!has_stream_index(sub_params.stream_index())); + + subtitle_streams_.push_back(sub_params); + } + + Track::Type get_type_of_stream(int index) + { + if (stream_is_video(index)) { + return Track::k_video; + } else if (stream_is_audio(index)) { + return Track::k_audio; + } else if (stream_is_subtitle(index)) { + return Track::k_subtitle; + } else { + return Track::k_none; + } + } + + bool stream_is_video(int index) const + { + for (const VideoParams &vp : video_streams_) { + if (vp.stream_index() == index) { + return true; + } + } + + return false; + } + + bool stream_is_audio(int index) const + { + for (const AudioParams &ap : audio_streams_) { + if (ap.stream_index() == index) { + return true; + } + } + + return false; + } + + bool stream_is_subtitle(int index) const + { + for (const SubtitleParams &sp : subtitle_streams_) { + if (sp.stream_index() == index) { + return true; + } + } + + return false; + } + + bool has_stream_index(int index) const + { + return stream_is_video(index) || stream_is_audio(index) || + stream_is_subtitle(index); + } + + int get_stream_count() const + { + return total_stream_count_; + } + void set_stream_count(int s) + { + total_stream_count_ = s; + } + + void set_source_start_time(const Rational &time, const std::string &source) + { + source_start_time_ = time; + source_start_time_source_ = source; + has_source_start_time_ = true; + } + + bool has_source_start_time() const + { + return has_source_start_time_; + } + + const Rational &source_start_time() const + { + return source_start_time_; + } + + const std::string &source_start_time_source() const + { + return source_start_time_source_; + } + + bool load(const std::string &filename); + + bool save(const std::string &filename) const; + + const std::vector &get_video_streams() const + { + return video_streams_; + } + std::vector &get_video_streams() + { + return video_streams_; + } + + const std::vector &get_audio_streams() const + { + return audio_streams_; + } + std::vector &get_audio_streams() + { + return audio_streams_; + } + + const std::vector &get_subtitle_streams() const + { + return subtitle_streams_; + } + std::vector &get_subtitle_streams() + { + return subtitle_streams_; + } + +private: + static constexpr unsigned k_footage_meta_version = 7; + + std::string decoder_; + + std::vector video_streams_; + + std::vector audio_streams_; + + std::vector subtitle_streams_; + + int total_stream_count_; + + Rational source_start_time_; + + std::string source_start_time_source_; + + bool has_source_start_time_; +}; + +} + +#endif // OAK_FOOTAGEDESCRIPTION_H diff --git a/src/node/src/project/sequence/CMakeLists.txt b/src/node/src/project/sequence/CMakeLists.txt new file mode 100644 index 000000000..7778e502c --- /dev/null +++ b/src/node/src/project/sequence/CMakeLists.txt @@ -0,0 +1,23 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/project/sequence/sequence.h + node/project/sequence/sequence.cpp + PARENT_SCOPE +) diff --git a/src/node/src/project/sequence/sequence.cpp b/src/node/src/project/sequence/sequence.cpp new file mode 100644 index 000000000..439f43746 --- /dev/null +++ b/src/node/src/project/sequence/sequence.cpp @@ -0,0 +1,221 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "sequence.h" + +#include "timeline/timelineundogeneral.h" + +namespace olive +{ + +const std::string Sequence::k_track_input_format = "track_in_%1"; + +#define super ViewerOutput + +// Replaces k_track_input_format.arg(i) +static std::string track_input_id_for(int i) +{ + std::string s = Sequence::k_track_input_format; + std::string::size_type pos = s.find("%1"); + if (pos != std::string::npos) { + s.replace(pos, 2, std::to_string(i)); + } + return s; +} + +Sequence::Sequence() +{ + set_flag(k_is_item); + + // Create TrackList instances + track_lists_.resize(Track::k_count); + + for (int i = 0; i < Track::k_count; i++) { + // Create track input + std::string track_input_id = track_input_id_for(i); + + add_input(track_input_id, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable | k_input_flag_array | + k_input_flag_hidden | k_input_flag_ignore_invalidations)); + + TrackList *list = + new TrackList(this, static_cast(i), track_input_id); + track_lists_[i] = list; + + // NOTE(de-Qt): the TrackList signal connections were removed with + // QObject. Whoever de-Qt's TrackList must call, from the former + // signal emission points: + // track_list_changed -> sequence->update_track_cache() + // length_changed -> sequence->verify_length() + // (The track_added/track_removed relay signals were deleted.) + } +} + +Sequence::~Sequence() +{ + // Match NODE_DEFAULT_DESTRUCTOR: disconnect while still a Sequence + disconnect_all(); + + // Owns the TrackLists created in the constructor (formerly QObject + // children of this Sequence) + for (TrackList *list : track_lists_) { + delete list; + } +} + +void Sequence::add_default_nodes(MultiUndoCommand *command) +{ + // Create tracks and connect them to the viewer + UndoCommand *video_track_command = + new TimelineAddTrackCommand(track_list(Track::k_video)); + UndoCommand *audio_track_command = + new TimelineAddTrackCommand(track_list(Track::k_audio)); + + if (command) { + command->add_child(video_track_command); + command->add_child(audio_track_command); + } else { + video_track_command->redo_now(); + audio_track_command->redo_now(); + delete video_track_command; + delete audio_track_command; + } +} + +Variant Sequence::data(const DataType &d) const +{ + if (d == icon) { + return "sequence"; + } + + return super::data(d); +} + +std::vector Sequence::get_unlocked_tracks() const +{ + std::vector tracks = get_tracks(); + + for (int i = 0; i < int(tracks.size()); i++) { + if (tracks.at(i)->is_locked()) { + tracks.erase(tracks.begin() + i); + i--; + } + } + + return tracks; +} + +void Sequence::retranslate() +{ + super::retranslate(); + + for (int i = 0; i < Track::k_count; i++) { + std::string input_name; + + switch (static_cast(i)) { + case Track::k_video: + input_name = "Video Tracks"; + break; + case Track::k_audio: + input_name = "Audio Tracks"; + break; + case Track::k_subtitle: + input_name = "Subtitle Tracks"; + break; + case Track::k_none: + case Track::k_count: + break; + } + + if (!input_name.empty()) { + set_input_name(track_input_id_for(i), input_name); + } + } +} + +void Sequence::invalidate_cache(const TimeRange &range, const std::string &from, + int element, InvalidateCacheOptions options) +{ + // (The subtitles_changed() signal for the subtitle track input was + // removed with QObject; observers are notified through the facade layer.) + + super::invalidate_cache(range, from, element, options); +} + +Rational Sequence::verify_length_internal(Track::Type type) const +{ + if (!track_lists_.empty()) { + switch (type) { + case Track::k_video: + return track_lists_.at(Track::k_video)->get_total_length(); + case Track::k_audio: + return track_lists_.at(Track::k_audio)->get_total_length(); + case Track::k_subtitle: + return track_lists_.at(Track::k_subtitle)->get_total_length(); + case Track::k_none: + case Track::k_count: + break; + } + } + + return 0; +} + +void Sequence::InputConnectedEvent(const std::string &input, int element, + Node *output) +{ + for (TrackList *list : track_lists_) { + if (list->track_input() == input) { + // Return because we found our input + list->track_connected(output, element); + return; + } + } + + super::InputConnectedEvent(input, element, output); +} + +void Sequence::InputDisconnectedEvent(const std::string &input, int element, + Node *output) +{ + for (TrackList *list : track_lists_) { + if (list->track_input() == input) { + // Return because we found our input + list->track_disconnected(output, element); + return; + } + } + + super::InputDisconnectedEvent(input, element, output); +} + +void Sequence::update_track_cache() +{ + track_cache_.clear(); + + for (TrackList *list : track_lists_) { + for (Track *track : list->get_tracks()) { + track_cache_.push_back(track); + } + } +} + +} diff --git a/src/node/src/project/sequence/sequence.h b/src/node/src/project/sequence/sequence.h new file mode 100644 index 000000000..d468f362b --- /dev/null +++ b/src/node/src/project/sequence/sequence.h @@ -0,0 +1,131 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SEQUENCE_H +#define OAK_SEQUENCE_H + +#include "output/track/tracklist.h" +#include "output/viewer/viewer.h" + +namespace olive +{ + +/** + * @brief The main timeline object, an graph of edited clips that forms a complete edit + */ +class Sequence : public ViewerOutput { +public: + Sequence(); + + // Deletes the owned TrackLists (raw pointers; they were QObject children + // in the Qt build). Custom destructor, so NODE_DEFAULT_FUNCTIONS is + // expanded manually. + virtual ~Sequence() override; + + virtual Node *copy() const override + { + return new Sequence(); + } + + virtual std::string name() const override + { + return "Sequence"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.sequence"; + } + + virtual std::vector category() const override + { + return { k_category_project }; + } + + virtual std::string description() const override + { + return "A series of cuts that result in an edited video. Also called " + "a timeline."; + } + + void add_default_nodes(MultiUndoCommand *command = nullptr); + + virtual Variant data(const DataType &d) const override; + + const std::vector &get_tracks() const + { + return track_cache_; + } + + Track *get_track_from_reference(const Track::Reference &track_ref) const + { + if (track_ref.type() < 0 || + track_ref.type() >= int(track_lists_.size())) { + return nullptr; + } + return track_lists_.at(track_ref.type())->get_track_at(track_ref.index()); + } + + /** + * @brief Same as GetTracks() but omits tracks that are locked. + */ + std::vector get_unlocked_tracks() const; + + TrackList *track_list(Track::Type type) const + { + return track_lists_.at(type); + } + + virtual void retranslate() override; + + virtual void invalidate_cache(const TimeRange &range, + const std::string &from, int element, + InvalidateCacheOptions options) override; + + /** + * @brief Rebuild the flat track cache from the track lists + * + * Formerly a private slot connected to TrackList::track_list_changed; + * the caller (facade / TrackList de-Qt wave) must now call this + * explicitly whenever a track list changes. + */ + void update_track_cache(); + + static const std::string k_track_input_format; + +protected: + virtual void InputConnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual void InputDisconnectedEvent(const std::string &input, int element, + Node *output) override; + + virtual Rational verify_length_internal(Track::Type type) const override; + +private: + std::vector track_lists_; + + std::vector track_cache_; +}; + +} + +#endif // OAK_SEQUENCE_H diff --git a/src/node/src/project/serializer/CMakeLists.txt b/src/node/src/project/serializer/CMakeLists.txt new file mode 100644 index 000000000..f6a48659b --- /dev/null +++ b/src/node/src/project/serializer/CMakeLists.txt @@ -0,0 +1,43 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/project/serializer/serializer.cpp + node/project/serializer/serializer.h + node/project/serializer/serializer190219.cpp + node/project/serializer/serializer190219.h + node/project/serializer/serializer210528.cpp + node/project/serializer/serializer210528.h + node/project/serializer/serializer210907.cpp + node/project/serializer/serializer210907.h + node/project/serializer/serializer211228.cpp + node/project/serializer/serializer211228.h + node/project/serializer/serializer220403.cpp + node/project/serializer/serializer220403.h + node/project/serializer/serializer230220.cpp + node/project/serializer/serializer230220.h + + + node/project/serializer/serializedlayoutinfo.cpp + node/project/serializer/serializedlayoutinfo.h + + node/project/serializer/typeserializer.cpp + node/project/serializer/typeserializer.h + + PARENT_SCOPE +) diff --git a/src/node/src/project/serializer/serializedlayoutinfo.cpp b/src/node/src/project/serializer/serializedlayoutinfo.cpp new file mode 100644 index 000000000..36afa60d7 --- /dev/null +++ b/src/node/src/project/serializer/serializedlayoutinfo.cpp @@ -0,0 +1,201 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "serializedlayoutinfo.h" + +#include + +namespace olive +{ + +void SerializedLayoutInfo::to_xml(XmlStreamWriter *writer) const +{ + writer->write_attribute("version", std::to_string(k_version)); + + writer->write_start_element("folders"); + + for (Folder *folder : open_folders) { + writer->write_text_element( + "folder", std::to_string(reinterpret_cast(folder))); + } + + writer->write_end_element(); // folders + + writer->write_start_element("timeline"); + + for (Sequence *sequence : open_sequences) { + writer->write_text_element( + "sequence", + std::to_string(reinterpret_cast(sequence))); + } + + writer->write_end_element(); // timeline + + writer->write_start_element("viewers"); + + for (ViewerOutput *viewer : open_viewers) { + writer->write_text_element( + "viewer", std::to_string(reinterpret_cast(viewer))); + } + + writer->write_end_element(); // viewers + + writer->write_start_element("data"); + + for (auto it = panel_data.cbegin(); it != panel_data.cend(); it++) { + writer->write_start_element("panel"); + + writer->write_attribute("id", it->first); + + const PanelLayoutInfo &info = it->second; + for (auto jt = info.cbegin(); jt != info.cend(); jt++) { + writer->write_start_element("option"); + + writer->write_attribute("name", jt->first); + + writer->write_characters(jt->second); + + writer->write_end_element(); // option + } + + writer->write_end_element(); // panel + } + + writer->write_end_element(); // data + + writer->write_text_element("state", byte_array_to_base64(state)); +} + +SerializedLayoutInfo +SerializedLayoutInfo::from_xml(XmlStreamReader *reader, + const std::map &node_ptrs) +{ + SerializedLayoutInfo info; + + unsigned int file_version = 0; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "version") { + file_version = strtoul(attr.value.c_str(), nullptr, 10); + } + } + + // Really basic version checking, in the future we may use this to parse multiple versions + if (file_version != k_version) { + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "folders") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "folder") { + uintptr_t item_id = strtoull( + reader->read_element_text().c_str(), nullptr, 10); + + auto it = node_ptrs.find(item_id); + Folder *open_item = static_cast( + it != node_ptrs.end() ? it->second : nullptr); + info.open_folders.push_back(open_item); + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "timeline") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "sequence") { + uintptr_t item_id = strtoull( + reader->read_element_text().c_str(), nullptr, 10); + + auto it = node_ptrs.find(item_id); + Sequence *open_seq = static_cast( + it != node_ptrs.end() ? it->second : nullptr); + info.open_sequences.push_back(open_seq); + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "viewers") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "viewer") { + uintptr_t item_id = strtoull( + reader->read_element_text().c_str(), nullptr, 10); + + auto it = node_ptrs.find(item_id); + ViewerOutput *open_viewer = static_cast( + it != node_ptrs.end() ? it->second : nullptr); + info.open_viewers.push_back(open_viewer); + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "state") { + info.state = + byte_array_from_base64(reader->read_element_text()); + + } else if (reader->name() == "data") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "panel") { + std::string id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (!id.empty()) { + PanelLayoutInfo i; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "option") { + std::string name; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "name") { + name = attr.value; + } + } + + if (!name.empty()) { + i[name] = reader->read_element_text(); + } + } else { + reader->skip_current_element(); + } + } + + info.panel_data[id] = i; + } + + } else { + reader->skip_current_element(); + } + } + + } else { + reader->skip_current_element(); + } + } + + return info; +} + +} diff --git a/src/node/src/project/serializer/serializedlayoutinfo.h b/src/node/src/project/serializer/serializedlayoutinfo.h new file mode 100644 index 000000000..e1450a2e1 --- /dev/null +++ b/src/node/src/project/serializer/serializedlayoutinfo.h @@ -0,0 +1,71 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_SERIALIZEDLAYOUTINFO_H +#define OAK_SERIALIZEDLAYOUTINFO_H + +#include + +#include "project/folder/folder.h" +#include "project/sequence/sequence.h" + +namespace olive +{ + +/** + * @brief Per-panel layout data (key/value pairs) + * + * Identical to PanelWidget::Info in the UI layer; defined here so the + * engine-side layout data structure does not depend on widget headers. + */ +using PanelLayoutInfo = std::map; + +/** + * @brief Plain data container for a serialized main window layout + * + * Pure data structure with no behavior beyond XML (de)serialization, so + * consumers (app, tests) can use it without pulling in any engine-side + * C++ symbols. + */ +class SerializedLayoutInfo { +public: + SerializedLayoutInfo() = default; + + void to_xml(XmlStreamWriter *writer) const; + + static SerializedLayoutInfo + from_xml(XmlStreamReader *reader, + const std::map &node_map); + + ByteArray state; + + std::vector open_folders; + + std::vector open_sequences; + + std::vector open_viewers; + + std::map panel_data; + +private: + static const unsigned int k_version = 1; +}; + +} + +#endif // OAK_SERIALIZEDLAYOUTINFO_H diff --git a/src/node/src/project/serializer/serializer.cpp b/src/node/src/project/serializer/serializer.cpp new file mode 100644 index 000000000..204ddea73 --- /dev/null +++ b/src/node/src/project/serializer/serializer.cpp @@ -0,0 +1,391 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer.h" + +#include +#include +#include +#include +#include + +#include + +#include "filefunctions.h" +#include "xmlutils.h" +#include "coreengine.h" +#include "group/group.h" +#include "serializer190219.h" +#include "serializer210528.h" +#include "serializer210907.h" +#include "serializer211228.h" +#include "serializer220403.h" +#include "serializer230220.h" + +namespace olive +{ + +std::vector ProjectSerializer::instances; + +namespace +{ + +// Qt qCompress()-compatible: 4-byte big-endian uncompressed size, followed +// by zlib deflate data. Must stay byte-compatible with existing .ove files. +std::string qcompress_compat(const std::string &in) +{ + uLongf bound = compressBound(in.size()); + std::string out; + out.resize(4 + bound); + out[0] = char((in.size() >> 24) & 0xFF); + out[1] = char((in.size() >> 16) & 0xFF); + out[2] = char((in.size() >> 8) & 0xFF); + out[3] = char(in.size() & 0xFF); + uLongf dest_len = bound; + if (compress2(reinterpret_cast(&out[4]), &dest_len, + reinterpret_cast(in.data()), in.size(), + Z_DEFAULT_COMPRESSION) != Z_OK) { + return std::string(); + } + out.resize(4 + dest_len); + return out; +} + +// Qt qUncompress()-compatible counterpart of qcompress_compat() +bool quncompress_compat(const std::string &in, std::string *out) +{ + if (in.size() < 4) { + return false; + } + uLongf len = (uLongf(uint8_t(in[0])) << 24) | + (uLongf(uint8_t(in[1])) << 16) | + (uLongf(uint8_t(in[2])) << 8) | uLongf(uint8_t(in[3])); + out->resize(len); + if (uncompress(reinterpret_cast(&(*out)[0]), &len, + reinterpret_cast(in.data() + 4), + in.size() - 4) != Z_OK) { + return false; + } + out->resize(len); + return true; +} + +} // namespace + +void ProjectSerializer::initialize() +{ + // Make sure to order these from oldest to newest + + // FIXME: Implement this - yes it's a 0.1 project loader + //instances_.append(new ProjectSerializer190219); + + instances.push_back(new ProjectSerializer210528); + instances.push_back(new ProjectSerializer210907); + instances.push_back(new ProjectSerializer211228); + instances.push_back(new ProjectSerializer220403); + instances.push_back(new ProjectSerializer230220); +} + +void ProjectSerializer::destroy() +{ + for (ProjectSerializer *s : instances) { + delete s; + } + instances.clear(); +} + +ProjectSerializer::Result ProjectSerializer::load(Project *project, + const std::string &filename, + LoadType load_type) +{ + std::ifstream project_file(filename, std::ios::binary); + + if (project_file.is_open()) { + std::stringstream ss; + ss << project_file.rdbuf(); + std::string file_data = ss.str(); + project_file.close(); + + // Some project files are compressed, marked with "OVEC" at the beginning of the file. Check for + // that signature now. + std::string xml_data; + if (file_data.size() >= 4 && + !memcmp(file_data.data(), "OVEC", 4)) { + // File is compressed, decompress into memory + if (!quncompress_compat(file_data.substr(4), &xml_data)) { + Result r(k_xml_error); + r.set_details("Failed to decompress project file"); + return r; + } + } else { + xml_data = std::move(file_data); + } + + XmlStreamReader reader(xml_data); + + Result inner_result = load(project, &reader, load_type); + + if (inner_result.code() != k_success) { + return inner_result; + } + + if (reader.has_error()) { + Result r(k_xml_error); + r.set_details(reader.error_string()); + return r; + } else { + return inner_result; + } + } else { + Result r(k_file_error); + r.set_details("Unable to open '" + filename + "'"); + return r; + } +} + +ProjectSerializer::Result ProjectSerializer::load(Project *project, + XmlStreamReader *reader, + LoadType load_type) +{ + // Determine project version + uint version = 0; + Result res = k_unknown_version; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "olive" || + reader->name() == "project") { // 0.1 projects only + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "version") { // 230220+ projects + version = strtoul(attr.value.c_str(), nullptr, 10); + } else if (attr.name == "url") { // 230220+ projects + if (project) { + project->set_saved_url(attr.value); + } + } + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "version") { // projects <= 220403 + version = strtoul(reader->read_element_text().c_str(), + nullptr, 10); + } else if (reader->name() == "url") { // projects <= 220403 + if (project) { + project->set_saved_url(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } else { + // Handle any other value with the serializer + res = load_with_serializer_version(version, project, reader, + load_type); + } + } + } else { + reader->skip_current_element(); + } + } + + return res; +} + +ProjectSerializer::Result ProjectSerializer::paste(LoadType load_type, + Project *project) +{ + // ADAPT(M9): engine/coreengine.h is still Qt-based; convert at the + // boundary until it is de-Qt'd. + std::string clipboard = + EngineCore::paste_string_from_clipboard(); + if (clipboard.empty()) { + return k_no_data; + } + + XmlStreamReader reader(clipboard); + + return ProjectSerializer::load(project, &reader, load_type); +} + +ProjectSerializer::Result ProjectSerializer::save(const SaveData &data, + bool compress) +{ + std::string temp_save = + FileFunctions::get_safe_temporary_filename(data.get_filename()); + + XmlStreamWriter writer; + + Result inner_result = save(&writer, data); + + if (inner_result != k_success) { + return inner_result; + } + + std::ofstream project_file(temp_save, + std::ios::binary | std::ios::trunc); + + if (project_file.is_open()) { + if (compress) { + project_file.write("OVEC", 4); + std::string compressed = qcompress_compat(writer.output()); + project_file.write(compressed.data(), compressed.size()); + } else { + project_file << writer.output(); + } + + project_file.close(); + + // Save was successful, we can now rewrite the original file + if (FileFunctions::rename_file_allow_overwrite(temp_save, + data.get_filename())) { + return k_success; + } else { + Result r(k_overwrite_error); + r.set_details(temp_save); + return r; + } + } else { + Result r(k_file_error); + r.set_details(temp_save); + return r; + } +} + +ProjectSerializer::Result ProjectSerializer::save(XmlStreamWriter *writer, + const SaveData &data) +{ + // NOTE(de-Qt): no writeStartDocument()/auto-formatting; the output is + // compact XML without a declaration. Element/attribute names and order + // are unchanged. + + writer->write_start_element("olive"); + + // By default, save as last serializer which, assuming the instances are ordered correctly, + // will be the newest file format. But we may allow saving as older versions later on. + ProjectSerializer *serializer = instances.back(); + + // Version is stored in YYMMDD from whenever the project format was last changed + // Allows easy integer math for checking project versions. + writer->write_attribute("version", std::to_string(serializer->version())); + + if (!data.get_filename().empty()) { + writer->write_attribute("url", data.get_filename()); + } + + serializer->save(writer, data, nullptr); + + writer->write_end_element(); // olive + + writer->write_end_document(); + + return k_success; +} + +ProjectSerializer::Result ProjectSerializer::copy(const SaveData &data) +{ + XmlStreamWriter writer; + + ProjectSerializer::Result res = ProjectSerializer::save(&writer, data); + + if (res == k_success) { + // ADAPT(M9): engine/coreengine.h is still Qt-based + EngineCore::copy_string_to_clipboard( + writer.output()); + } + + return res; +} + +bool ProjectSerializer::check_compressed_id(const std::string &filename) +{ + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + return false; + } + char b[4] = { 0, 0, 0, 0 }; + file.read(b, 4); + return !memcmp(b, "OVEC", 4); +} + +bool ProjectSerializer::is_cancelled() const +{ + return false; +} + +ProjectSerializer::Result +ProjectSerializer::load_with_serializer_version(uint version, Project *project, + XmlStreamReader *reader, + LoadType load_type) +{ + // Failed to find version in file + if (version == 0) { + return k_unknown_version; + } + + // We should now have the version, if we have a serializer for it, use it to load the project + ProjectSerializer *serializer = nullptr; + + for (ProjectSerializer *s : instances) { + if (version == s->version()) { + serializer = s; + break; + } else if (version < s->version()) { + // Assuming the instance list is in order, if the project version is less than any version + // we find, we must not support it anymore + return k_project_too_old; + } + } + + if (serializer) { + LoadData ld = serializer->load(project, reader, load_type, nullptr); + Result r(k_success); + if (reader->has_error()) { + r = Result(k_xml_error); + // NOTE(de-Qt): no line number available from XmlStreamReader + r.set_details(reader->error_string()); + } + r.set_load_data(ld); + return r; + } else { + // Reached the end of the list with no serializer, assume too new + return k_project_too_new; + } +} + +void ProjectSerializer::SaveData::set_only_serialize_nodes_and_resolve_groups( + std::vector nodes) +{ + // For any groups, add children + for (int i = 0; i < int(nodes.size()); i++) { + // If this is a group, add the child nodes too + if (NodeGroup *g = dynamic_cast(nodes.at(i))) { + for (auto it = g->get_context_positions().cbegin(); + it != g->get_context_positions().cend(); it++) { + if (std::find(nodes.begin(), nodes.end(), it->first) == + nodes.end()) { + nodes.push_back(it->first); + } + } + } + } + + set_only_serialize_nodes(nodes); +} + +} diff --git a/src/node/src/project/serializer/serializer.h b/src/node/src/project/serializer/serializer.h new file mode 100644 index 000000000..dcd5a7dba --- /dev/null +++ b/src/node/src/project/serializer/serializer.h @@ -0,0 +1,293 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PROJECTSERIALIZER_H +#define OAK_PROJECTSERIALIZER_H + +#include +#include +#include +#include + +#include "define.h" +#include "project.h" +#include "project/serializer/serializedlayoutinfo.h" +#include "typeserializer.h" + +namespace olive +{ + +/** + * @brief An abstract base class for serializing/deserializing project data + * + * The goal of this is to further abstract serialized project data from their + * in-memory representations. + */ +class ProjectSerializer { +public: + enum LoadType { + k_project, + k_only_nodes, + k_only_clips, + k_only_markers, + k_only_keyframes + }; + + ProjectSerializer() = default; + + virtual ~ProjectSerializer() + { + } + + DISABLE_COPY_MOVE(ProjectSerializer) + + enum ResultCode { + k_success, + k_project_too_old, + k_project_too_new, + k_unknown_version, + k_file_error, + k_xml_error, + k_overwrite_error, + k_no_data + }; + + using SerializedProperties = + std::map>; + using SerializedKeyframes = + std::map>; + + class LoadData { + public: + LoadData() = default; + + SerializedProperties properties; + + std::vector markers; + + SerializedKeyframes keyframes; + + SerializedLayoutInfo layout; + + std::vector nodes; + + std::map node_ptrs; + + std::map node_uuids; + + Node::OutputConnections promised_connections; + }; + + class Result { + public: + Result(const ResultCode &code) + : code_(code) + { + } + + bool operator==(const ResultCode &code) + { + return code_ == code; + } + bool operator!=(const ResultCode &code) + { + return code_ != code; + } + + const ResultCode &code() const + { + return code_; + } + + const std::string &get_details() const + { + return details_; + } + + void set_details(const std::string &s) + { + details_ = s; + } + + const LoadData &get_load_data() const + { + return load_data_; + } + + void set_load_data(const LoadData &p) + { + load_data_ = p; + } + + private: + ResultCode code_; + + std::string details_; + + LoadData load_data_; + }; + + class SaveData { + public: + SaveData(LoadType type, Project *project = nullptr, + const std::string &filename = std::string()) + { + type_ = type; + project_ = project; + filename_ = filename; + } + + Project *get_project() const + { + return project_; + } + void set_project(Project *p) + { + project_ = p; + } + + const std::string &get_filename() const + { + return filename_; + } + void set_filename(const std::string &s) + { + filename_ = s; + } + + LoadType type() const + { + return type_; + } + + const SerializedLayoutInfo &get_layout() const + { + return layout_; + } + void set_layout(const SerializedLayoutInfo &layout) + { + layout_ = layout; + } + + const std::vector &get_only_serialize_nodes() const + { + return only_serialize_nodes_; + } + void set_only_serialize_nodes(const std::vector &only) + { + only_serialize_nodes_ = only; + } + void set_only_serialize_nodes_and_resolve_groups( + std::vector only); + + const std::vector &get_only_serialize_markers() const + { + return only_serialize_markers_; + } + void set_only_serialize_markers( + const std::vector &only) + { + only_serialize_markers_ = only; + } + + const std::vector &get_only_serialize_keyframes() const + { + return only_serialize_keyframes_; + } + void set_only_serialize_keyframes( + const std::vector &only) + { + only_serialize_keyframes_ = only; + } + + const SerializedProperties &get_properties() const + { + return properties_; + } + void set_properties(const SerializedProperties &p) + { + properties_ = p; + } + + private: + LoadType type_; + + Project *project_; + + std::string filename_; + + SerializedLayoutInfo layout_; + + std::vector only_serialize_nodes_; + + SerializedProperties properties_; + + std::vector only_serialize_markers_; + + std::vector only_serialize_keyframes_; + }; + + static void initialize(); + + static void destroy(); + + static Result load(Project *project, const std::string &filename, + LoadType load_type); + static Result load(Project *project, XmlStreamReader *read_device, + LoadType load_type); + static Result paste(LoadType load_type, Project *project = nullptr); + + static Result save(const SaveData &data, bool compress); + static Result save(XmlStreamWriter *write_device, const SaveData &data); + static Result copy(const SaveData &data); + + /** + * @brief Returns true if the file starts with the "OVEC" compressed + * project signature + * + * NOTE(de-Qt): took a QFile* before; now takes the file path. + */ + static bool check_compressed_id(const std::string &filename); + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const = 0; + + virtual void save(XmlStreamWriter *writer, const SaveData &data, + void *reserved) const + { + } + + virtual uint version() const = 0; + + bool is_cancelled() const; + +private: + static Result load_with_serializer_version(uint version, Project *project, + XmlStreamReader *reader, + LoadType load_type); + + static std::vector instances; +}; + +} + +#endif // OAK_PROJECTSERIALIZER_H diff --git a/src/node/src/project/serializer/serializer190219.cpp b/src/node/src/project/serializer/serializer190219.cpp new file mode 100644 index 000000000..17d0c39a0 --- /dev/null +++ b/src/node/src/project/serializer/serializer190219.cpp @@ -0,0 +1,34 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer190219.h" + +namespace olive +{ + +ProjectSerializer::LoadData +ProjectSerializer190219::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + return LoadData(); +} + +} diff --git a/src/node/src/project/serializer/serializer190219.h b/src/node/src/project/serializer/serializer190219.h new file mode 100644 index 000000000..a33812d58 --- /dev/null +++ b/src/node/src/project/serializer/serializer190219.h @@ -0,0 +1,46 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PROJECTSERIALIZER190219_H +#define OAK_PROJECTSERIALIZER190219_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer190219 : public ProjectSerializer { +public: + ProjectSerializer190219() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual uint version() const override + { + return 190219; + } +}; + +} + +#endif // SERIALIZER190219_H diff --git a/src/node/src/project/serializer/serializer210528.cpp b/src/node/src/project/serializer/serializer210528.cpp new file mode 100644 index 000000000..6523ffbf2 --- /dev/null +++ b/src/node/src/project/serializer/serializer210528.cpp @@ -0,0 +1,876 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer210528.h" + +#include +#include + +#include "config/config.h" +#include "factory.h" +#include "group/group.h" + +namespace olive +{ + +ProjectSerializer210528::LoadData +ProjectSerializer210528::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + XMLNodeData xml_node_data; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "uuid") { + project->set_uuid(reader->read_element_text()); + + } else if (reader->name() == "nodes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + std::string id; + + { + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } else if (attr.name == "root" && + attr.value == "1") { + is_root = true; + } else if (attr.name == "cm" && attr.value == "1") { + is_cm = true; + } else if (attr.name == "settings" && + attr.value == "1") { + is_settings = true; + } + } + } + + if (id.empty()) { + fprintf(stderr, "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + Node *node; + bool handled_elsewhere = false; + + if (is_root) { + project->initialize(); + node = project->root(); + } else if (is_cm) { + load_color_manager(reader, project); + handled_elsewhere = true; + } else if (is_settings) { + load_project_settings(reader, project); + handled_elsewhere = true; + } else { + node = NodeFactory::create_from_id(id); + } + + if (!handled_elsewhere) { + if (!node) { + fprintf(stderr, + "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + load_node(node, xml_node_data, reader); + project->add_node(node); + } + } + } + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "positions") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "context") { + uintptr_t context_ptr = 0; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + context_ptr = + strtoull(attr.value.c_str(), nullptr, 10); + break; + } + } + + auto ctx_it = xml_node_data.node_ptrs.find(context_ptr); + Node *context = ctx_it != xml_node_data.node_ptrs.end() ? + ctx_it->second : + nullptr; + + if (!context) { + fprintf(stderr, "Failed to find pointer for context\n"); + reader->skip_current_element(); + } else { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t node_ptr; + Node::Position node_pos; + + if (load_position(reader, &node_ptr, + &node_pos)) { + auto node_it = + xml_node_data.node_ptrs.find(node_ptr); + Node *node = + node_it != xml_node_data.node_ptrs + .end() ? + node_it->second : + nullptr; + + if (node) { + context->set_node_position_in_context( + node, node_pos); + } else { + fprintf(stderr, + "Failed to find pointer for " + "node position\n"); + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } + + } else { + reader->skip_current_element(); + } + } + + } else { + // Skip this + reader->skip_current_element(); + } + } + + // Make connections + post_connect(xml_node_data); + + // Resolve tracks + for (Node *n : project->nodes()) { + n->set_caches_enabled(true); + + if (Track *t = dynamic_cast(n)) { + for (int i = 0; i < t->input_array_size(Track::k_block_input); i++) { + Block *b = static_cast( + t->get_connected_output(Track::k_block_input, i)); + if (!b->track()) { + t->append_block(b); + } + } + } + } + + return LoadData(); +} + +void ProjectSerializer210528::load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const +{ + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "input") { + load_input(node, reader, xml_node_data); + } else if (reader->name() == "ptr") { + xml_node_data.node_ptrs.insert( + { strtoull(reader->read_element_text().c_str(), nullptr, 10), + node }); + } else if (reader->name() == "label") { + node->set_label(reader->read_element_text()); + } else if (reader->name() == "color") { + node->set_override_color( + atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "links") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "link") { + xml_node_data.block_links.push_back( + { node, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "custom") { + load_node_custom(reader, node, xml_node_data); + + } else if (reader->name() == "connections") { + // Load connections + while (xml_read_next_start_element(reader)) { + if (reader->name() == "connection") { + std::string param_id; + int ele = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "element") { + ele = atoi(attr.value.c_str()); + } else if (attr.name == "input") { + param_id = attr.value; + } + } + + std::string output_node_id; + std::string output_param_id; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + output_node_id = reader->read_element_text(); + } else if (reader->name() == "output") { + output_param_id = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.desired_connections.push_back( + { NodeInput(node, param_id, ele), + strtoull(output_node_id.c_str(), nullptr, 10), + output_param_id }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "hints") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "hint") { + std::string input; + int element = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "input") { + input = attr.value; + } else if (attr.name == "element") { + element = atoi(attr.value.c_str()); + } + } + + Node::ValueHint vh; + load_value_hint(&vh, reader); + node->set_value_hint_for_input(input, vh, element); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + node->LoadFinishedEvent(); +} + +void ProjectSerializer210528::load_color_manager(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "config" || id == "default_input" || + id == "reference_space") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "default_input") { + // Default color space + // NOTE: Stupidly, we saved these as integers which means we can't add anything to the OCIO + // config. So we must convert back to string here. + static const std::vector list = { + "Linear", + "CIE-XYZ D65", + "Filmic Log Encoding", + "sRGB OETF", + "Apple DCI-P3 D65", + "AppleP3 sRGB OETF", + "BT.1886 EOTF", + "AppleP3 Filmic Log Encoding", + "BT.1886 Filmic Log Encoding", + "Fuji F-Log OETF", + "Fuji F-Log F-Gamut", + "Panasonic V-Log V-Gamut", + "Arri Wide Gamut / LogC EI 800", + "Arri Wide Gamut / LogC EI 400", + "Blackmagic Film Wide Gamut (Gen 5)", + "Rec.709 OETF", + "Non-Colour Data" + }; + int num_value = atoi(value.c_str()); + value = list.at(num_value); + project->set_default_input_color_space(value); + } else if (id == "reference_space") { + // Reference space + if (value == "1") { + value = ocio::ROLE_COMPOSITING_LOG; + } else { + value = ocio::ROLE_SCENE_LINEAR; + } + project->set_color_reference_space(value); + } else { + // Config filename + project->set_color_config_filename(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210528::load_project_settings(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "cache_setting" || id == "cache_path") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "cache_setting") { + project->set_cache_location_setting( + static_cast( + atoi(value.c_str()))); + } else { + project->set_custom_cache_path(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210528::load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const +{ + std::string param_id; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + param_id = attr.value; + + break; + } + } + + if (param_id.empty()) { + fprintf(stderr, "Failed to load parameter with missing ID\n"); + reader->skip_current_element(); + return; + } + + if (!node->has_input_with_id(param_id)) { + fprintf(stderr, "Failed to load parameter that didn't exist: %s\n", + param_id.c_str()); + reader->skip_current_element(); + return; + } + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "primary") { + // Load primary immediate + load_immediate(reader, node, param_id, -1, xml_node_data); + } else if (reader->name() == "subelements") { + // Load subelements + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "count") { + node->input_array_resize(param_id, + atoi(attr.value.c_str())); + } + } + + int element_counter = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + load_immediate(reader, node, param_id, element_counter, + xml_node_data); + + element_counter++; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210528::load_immediate(XmlStreamReader *reader, + Node *node, + const std::string &input, + int element, + XMLNodeData &xml_node_data) const +{ + (void) xml_node_data; + + NodeValue::Type data_type = node->get_input_data_type(input); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + // Load standard value + int val_index = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + Variant value_on_track; + + if (data_type == NodeValue::k_video_params) { + VideoParams vp; + vp.load(reader); + value_on_track = Variant::from_value(vp); + } else if (data_type == NodeValue::k_audio_params) { + AudioParams ap = + TypeSerializer::load_audio_params(reader); + value_on_track = Variant::from_value(ap); + } else { + std::string value_text = reader->read_element_text(); + + if (!value_text.empty()) { + value_on_track = NodeValue::string_to_value( + data_type, value_text, true); + } + } + + node->set_split_standard_value_on_track(input, val_index, + value_on_track, element); + + val_index++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "keyframing" && + node->is_input_keyframable(input)) { + node->set_input_is_keyframing( + input, atoi(reader->read_element_text().c_str()), element); + } else if (reader->name() == "keyframes") { + int track = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "key") { + std::string key_input; + Rational key_time; + NodeKeyframe::Type key_type = NodeKeyframe::k_linear; + Variant key_value; + PointF key_in_handle; + PointF key_out_handle; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (is_cancelled()) { + return; + } + + if (attr.name == "input") { + key_input = attr.value; + } else if (attr.name == "time") { + key_time = + Rational::from_string(attr.value); + } else if (attr.name == "type") { + key_type = static_cast( + atoi(attr.value.c_str())); + } else if (attr.name == "inhandlex") { + key_in_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "inhandley") { + key_in_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandlex") { + key_out_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandley") { + key_out_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } + } + + key_value = NodeValue::string_to_value( + data_type, reader->read_element_text(), true); + + NodeKeyframe *key = new NodeKeyframe( + key_time, key_value, key_type, track, element, + key_input, node); + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); + + // Replaces the former QObject setParent() (which + // registered the keyframe through childEvent) + node->add_keyframe(key); + } else { + reader->skip_current_element(); + } + } + + track++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "csinput") { + node->set_input_property(input, "col_input", + reader->read_element_text()); + } else if (reader->name() == "csdisplay") { + node->set_input_property(input, "col_display", + reader->read_element_text()); + } else if (reader->name() == "csview") { + node->set_input_property(input, "col_view", + reader->read_element_text()); + } else if (reader->name() == "cslook") { + node->set_input_property(input, "col_look", + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } +} + +bool ProjectSerializer210528::load_position(XmlStreamReader *reader, + uintptr_t *node_ptr, + Node::Position *pos) const +{ + bool got_node_ptr = false; + bool got_pos_x = false; + bool got_pos_y = false; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + *node_ptr = strtoull(attr.value.c_str(), nullptr, 10); + got_node_ptr = true; + break; + } + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "x") { + pos->position.set_x(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_x = true; + } else if (reader->name() == "y") { + pos->position.set_y(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_y = true; + } else if (reader->name() == "expanded") { + pos->expanded = atoi(reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + return got_node_ptr && got_pos_x && got_pos_y; +} + +void ProjectSerializer210528::post_connect(const XMLNodeData &xml_node_data) const +{ + for (const XMLNodeData::SerializedConnection &con : + xml_node_data.desired_connections) { + auto out_it = xml_node_data.node_ptrs.find(con.output_node); + if (out_it != xml_node_data.node_ptrs.end()) { + Node *out = out_it->second; + // Use output param as hint tag since we grandfathered those in + Node::ValueHint hint(con.output_param); + + Node::connect_edge(out, con.input); + + con.input.node()->set_value_hint_for_input(con.input.input(), hint, + con.input.element()); + } + } + + for (const XMLNodeData::BlockLink &l : xml_node_data.block_links) { + Node *a = l.block; + auto b_it = xml_node_data.node_ptrs.find(l.link); + Node *b = + b_it != xml_node_data.node_ptrs.end() ? b_it->second : nullptr; + + Node::link(a, b); + } + + for (const XMLNodeData::GroupLink &l : xml_node_data.group_input_links) { + auto in_it = xml_node_data.node_ptrs.find(l.input_node); + if (in_it != xml_node_data.node_ptrs.end()) { + Node *input_node = in_it->second; + NodeInput resolved(input_node, l.input_id, l.input_element); + + l.group->add_input_passthrough(resolved); + } + } + + for (auto it = xml_node_data.group_output_links.cbegin(); + it != xml_node_data.group_output_links.cend(); it++) { + auto out_it = xml_node_data.node_ptrs.find(it->second); + if (out_it != xml_node_data.node_ptrs.end()) { + it->first->set_output_passthrough(out_it->second); + } + } +} + +void ProjectSerializer210528::load_node_custom(XmlStreamReader *reader, + Node *node, + XMLNodeData &xml_node_data) const +{ + // Viewer-based nodes + if (ViewerOutput *viewer = dynamic_cast(node)) { + Footage *footage = dynamic_cast(node); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "points") { + load_timeline_points(reader, viewer); + } else if (reader->name() == "timestamp" && footage) { + footage->set_timestamp(strtoll( + reader->read_element_text().c_str(), nullptr, 10)); + } else { + reader->skip_current_element(); + } + } + + } else if (Track *track = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "height") { + track->set_track_height( + strtod(reader->read_element_text().c_str(), nullptr)); + } else { + reader->skip_current_element(); + } + } + + } else if (NodeGroup *group = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthroughs") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthrough") { + XMLNodeData::GroupLink link; + + link.group = group; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + link.input_node = strtoull( + reader->read_element_text().c_str(), + nullptr, 10); + } else if (reader->name() == "input") { + link.input_id = reader->read_element_text(); + } else if (reader->name() == "element") { + link.input_element = atoi( + reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.group_input_links.push_back(link); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "outputpassthrough") { + xml_node_data.group_output_links.insert( + { group, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + + } else { + reader->skip_current_element(); + } +} + +void ProjectSerializer210528::load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "markers") { + load_marker_list(reader, points->get_markers()); + } else if (reader->name() == "workarea") { + load_work_area(reader, points->get_work_area()); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210528::load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const +{ + Rational range_in = workarea->in(); + Rational range_out = workarea->out(); + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "enabled") { + workarea->set_enabled(attr.value != "0"); + } else if (attr.name == "in") { + range_in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + range_out = Rational::from_string(attr.value); + } + } + + TimeRange loaded_workarea(range_in, range_out); + + if (loaded_workarea != workarea->range()) { + workarea->set_range(loaded_workarea); + } + + reader->skip_current_element(); +} + +void ProjectSerializer210528::load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + std::string name; + Rational in, out; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "name") { + name = attr.value; + } else if (attr.name == "in") { + in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + out = Rational::from_string(attr.value); + } + } + + // ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker + // is de-Qt'd in M4 (must accept std::string name) + new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(), + TimeRange(in, out), name, markers); + } + + reader->skip_current_element(); + } +} + +void ProjectSerializer210528::load_value_hint(Node::ValueHint *hint, + XmlStreamReader *reader) const +{ + std::vector types; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "types") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "type") { + types.push_back(static_cast( + atoi(reader->read_element_text().c_str()))); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "index") { + hint->set_index(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "tag") { + hint->set_tag(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + hint->set_type(types); +} + +} diff --git a/src/node/src/project/serializer/serializer210528.h b/src/node/src/project/serializer/serializer210528.h new file mode 100644 index 000000000..56531eb81 --- /dev/null +++ b/src/node/src/project/serializer/serializer210528.h @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SERIALIZER210528_H +#define OAK_SERIALIZER210528_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer210528 : public ProjectSerializer { +public: + ProjectSerializer210528() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual uint version() const override + { + return 210528; + } + +private: + struct XMLNodeData { + struct SerializedConnection { + NodeInput input; + uintptr_t output_node; + std::string output_param; + }; + + struct BlockLink { + Node *block; + uintptr_t link; + }; + + struct GroupLink { + NodeGroup *group; + uintptr_t input_node; + std::string input_id; + int input_element; + }; + + std::map node_ptrs; + std::vector desired_connections; + std::vector block_links; + std::vector group_input_links; + std::map group_output_links; + }; + + void load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const; + + void load_color_manager(XmlStreamReader *reader, Project *project) const; + + void load_project_settings(XmlStreamReader *reader, Project *project) const; + + void load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const; + + void load_immediate(XmlStreamReader *reader, Node *node, + const std::string &input, int element, + XMLNodeData &xml_node_data) const; + + bool load_position(XmlStreamReader *reader, uintptr_t *node_ptr, + Node::Position *pos) const; + + void post_connect(const XMLNodeData &xml_node_data) const; + + void load_node_custom(XmlStreamReader *reader, Node *node, + XMLNodeData &xml_node_data) const; + + void load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const; + + void load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const; + + void load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const; + + void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; +}; + +} + +#endif // SERIALIZER211228_H diff --git a/src/node/src/project/serializer/serializer210907.cpp b/src/node/src/project/serializer/serializer210907.cpp new file mode 100644 index 000000000..90f293721 --- /dev/null +++ b/src/node/src/project/serializer/serializer210907.cpp @@ -0,0 +1,865 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer210907.h" + +#include +#include + +#include "config/config.h" +#include "factory.h" +#include "group/group.h" + +namespace olive +{ + +ProjectSerializer210907::LoadData +ProjectSerializer210907::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + XMLNodeData xml_node_data; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "uuid") { + project->set_uuid(reader->read_element_text()); + + } else if (reader->name() == "nodes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + std::string id; + + { + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } else if (attr.name == "root" && + attr.value == "1") { + is_root = true; + } else if (attr.name == "cm" && attr.value == "1") { + is_cm = true; + } else if (attr.name == "settings" && + attr.value == "1") { + is_settings = true; + } + } + } + + if (id.empty()) { + fprintf(stderr, "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + Node *node; + bool handled_elsewhere = false; + + if (is_root) { + project->initialize(); + node = project->root(); + } else if (is_cm) { + load_color_manager(reader, project); + handled_elsewhere = true; + } else if (is_settings) { + load_project_settings(reader, project); + handled_elsewhere = true; + } else { + node = NodeFactory::create_from_id(id); + } + + if (!handled_elsewhere) { + if (!node) { + fprintf(stderr, + "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + load_node(node, xml_node_data, reader); + project->add_node(node); + } + } + } + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "positions") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "context") { + uintptr_t context_ptr = 0; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + context_ptr = + strtoull(attr.value.c_str(), nullptr, 10); + break; + } + } + + auto ctx_it = xml_node_data.node_ptrs.find(context_ptr); + Node *context = ctx_it != xml_node_data.node_ptrs.end() ? + ctx_it->second : + nullptr; + + if (!context) { + fprintf(stderr, "Failed to find pointer for context\n"); + reader->skip_current_element(); + } else { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t node_ptr; + Node::Position node_pos; + + if (load_position(reader, &node_ptr, + &node_pos)) { + auto node_it = + xml_node_data.node_ptrs.find(node_ptr); + Node *node = + node_it != xml_node_data.node_ptrs + .end() ? + node_it->second : + nullptr; + + if (node) { + context->set_node_position_in_context( + node, node_pos); + } else { + fprintf(stderr, + "Failed to find pointer for " + "node position\n"); + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } + + } else { + reader->skip_current_element(); + } + } + + } else { + // Skip this + reader->skip_current_element(); + } + } + + // Make connections + post_connect(xml_node_data); + + // Resolve tracks + for (Node *n : project->nodes()) { + n->set_caches_enabled(true); + + if (Track *t = dynamic_cast(n)) { + for (int i = 0; i < t->input_array_size(Track::k_block_input); i++) { + Block *b = static_cast( + t->get_connected_output(Track::k_block_input, i)); + if (!b->track()) { + t->append_block(b); + } + } + } + } + + return LoadData(); +} + +void ProjectSerializer210907::load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const +{ + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "input") { + load_input(node, reader, xml_node_data); + } else if (reader->name() == "ptr") { + xml_node_data.node_ptrs.insert( + { strtoull(reader->read_element_text().c_str(), nullptr, 10), + node }); + } else if (reader->name() == "label") { + node->set_label(reader->read_element_text()); + } else if (reader->name() == "color") { + node->set_override_color( + atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "links") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "link") { + xml_node_data.block_links.push_back( + { node, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "custom") { + load_node_custom(reader, node, xml_node_data); + + } else if (reader->name() == "connections") { + // Load connections + while (xml_read_next_start_element(reader)) { + if (reader->name() == "connection") { + std::string param_id; + int ele = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "element") { + ele = atoi(attr.value.c_str()); + } else if (attr.name == "input") { + param_id = attr.value; + } + } + + std::string output_node_id; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "output") { + output_node_id = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.desired_connections.push_back( + { NodeInput(node, param_id, ele), + strtoull(output_node_id.c_str(), nullptr, 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "hints") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "hint") { + std::string input; + int element = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "input") { + input = attr.value; + } else if (attr.name == "element") { + element = atoi(attr.value.c_str()); + } + } + + Node::ValueHint vh; + load_value_hint(&vh, reader); + node->set_value_hint_for_input(input, vh, element); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + node->LoadFinishedEvent(); +} + +void ProjectSerializer210907::load_color_manager(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "config" || id == "default_input" || + id == "reference_space") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "default_input") { + // Default color space + // NOTE: Stupidly, we saved these as integers which means we can't add anything to the OCIO + // config. So we must convert back to string here. + static const std::vector list = { + "Linear", + "CIE-XYZ D65", + "Filmic Log Encoding", + "sRGB OETF", + "Apple DCI-P3 D65", + "AppleP3 sRGB OETF", + "BT.1886 EOTF", + "AppleP3 Filmic Log Encoding", + "BT.1886 Filmic Log Encoding", + "Fuji F-Log OETF", + "Fuji F-Log F-Gamut", + "Panasonic V-Log V-Gamut", + "Arri Wide Gamut / LogC EI 800", + "Arri Wide Gamut / LogC EI 400", + "Blackmagic Film Wide Gamut (Gen 5)", + "Rec.709 OETF", + "Non-Colour Data" + }; + int num_value = atoi(value.c_str()); + value = list.at(num_value); + project->set_default_input_color_space(value); + } else if (id == "reference_space") { + // Reference space + if (value == "1") { + value = ocio::ROLE_COMPOSITING_LOG; + } else { + value = ocio::ROLE_SCENE_LINEAR; + } + project->set_color_reference_space(value); + } else { + // Config filename + project->set_color_config_filename(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210907::load_project_settings(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "cache_setting" || id == "cache_path") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "cache_setting") { + project->set_cache_location_setting( + static_cast( + atoi(value.c_str()))); + } else { + project->set_custom_cache_path(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210907::load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const +{ + std::string param_id; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + param_id = attr.value; + + break; + } + } + + if (param_id.empty()) { + fprintf(stderr, "Failed to load parameter with missing ID\n"); + reader->skip_current_element(); + return; + } + + if (!node->has_input_with_id(param_id)) { + fprintf(stderr, "Failed to load parameter that didn't exist: %s\n", + param_id.c_str()); + reader->skip_current_element(); + return; + } + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "primary") { + // Load primary immediate + load_immediate(reader, node, param_id, -1, xml_node_data); + } else if (reader->name() == "subelements") { + // Load subelements + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "count") { + node->input_array_resize(param_id, + atoi(attr.value.c_str())); + } + } + + int element_counter = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + load_immediate(reader, node, param_id, element_counter, + xml_node_data); + + element_counter++; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210907::load_immediate(XmlStreamReader *reader, + Node *node, + const std::string &input, + int element, + XMLNodeData &xml_node_data) const +{ + (void) xml_node_data; + + NodeValue::Type data_type = node->get_input_data_type(input); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + // Load standard value + int val_index = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + Variant value_on_track; + + if (data_type == NodeValue::k_video_params) { + VideoParams vp; + vp.load(reader); + value_on_track = Variant::from_value(vp); + } else if (data_type == NodeValue::k_audio_params) { + AudioParams ap = + TypeSerializer::load_audio_params(reader); + value_on_track = Variant::from_value(ap); + } else { + std::string value_text = reader->read_element_text(); + + if (!value_text.empty()) { + value_on_track = NodeValue::string_to_value( + data_type, value_text, true); + } + } + + node->set_split_standard_value_on_track(input, val_index, + value_on_track, element); + + val_index++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "keyframing" && + node->is_input_keyframable(input)) { + node->set_input_is_keyframing( + input, atoi(reader->read_element_text().c_str()), element); + } else if (reader->name() == "keyframes") { + int track = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "key") { + std::string key_input; + Rational key_time; + NodeKeyframe::Type key_type = NodeKeyframe::k_linear; + Variant key_value; + PointF key_in_handle; + PointF key_out_handle; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (is_cancelled()) { + return; + } + + if (attr.name == "input") { + key_input = attr.value; + } else if (attr.name == "time") { + key_time = + Rational::from_string(attr.value); + } else if (attr.name == "type") { + key_type = static_cast( + atoi(attr.value.c_str())); + } else if (attr.name == "inhandlex") { + key_in_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "inhandley") { + key_in_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandlex") { + key_out_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandley") { + key_out_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } + } + + key_value = NodeValue::string_to_value( + data_type, reader->read_element_text(), true); + + NodeKeyframe *key = new NodeKeyframe( + key_time, key_value, key_type, track, element, + key_input, node); + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); + + // Replaces the former QObject setParent() (which + // registered the keyframe through childEvent) + node->add_keyframe(key); + } else { + reader->skip_current_element(); + } + } + + track++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "csinput") { + node->set_input_property(input, "col_input", + reader->read_element_text()); + } else if (reader->name() == "csdisplay") { + node->set_input_property(input, "col_display", + reader->read_element_text()); + } else if (reader->name() == "csview") { + node->set_input_property(input, "col_view", + reader->read_element_text()); + } else if (reader->name() == "cslook") { + node->set_input_property(input, "col_look", + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } +} + +bool ProjectSerializer210907::load_position(XmlStreamReader *reader, + uintptr_t *node_ptr, + Node::Position *pos) const +{ + bool got_node_ptr = false; + bool got_pos_x = false; + bool got_pos_y = false; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + *node_ptr = strtoull(attr.value.c_str(), nullptr, 10); + got_node_ptr = true; + break; + } + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "x") { + pos->position.set_x(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_x = true; + } else if (reader->name() == "y") { + pos->position.set_y(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_y = true; + } else if (reader->name() == "expanded") { + pos->expanded = atoi(reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + return got_node_ptr && got_pos_x && got_pos_y; +} + +void ProjectSerializer210907::post_connect(const XMLNodeData &xml_node_data) const +{ + for (const XMLNodeData::SerializedConnection &con : + xml_node_data.desired_connections) { + auto out_it = xml_node_data.node_ptrs.find(con.output_node); + if (out_it != xml_node_data.node_ptrs.end()) { + Node::connect_edge(out_it->second, con.input); + } + } + + for (const XMLNodeData::BlockLink &l : xml_node_data.block_links) { + Node *a = l.block; + auto b_it = xml_node_data.node_ptrs.find(l.link); + Node *b = + b_it != xml_node_data.node_ptrs.end() ? b_it->second : nullptr; + + Node::link(a, b); + } + + for (const XMLNodeData::GroupLink &l : xml_node_data.group_input_links) { + auto in_it = xml_node_data.node_ptrs.find(l.input_node); + if (in_it != xml_node_data.node_ptrs.end()) { + Node *input_node = in_it->second; + NodeInput resolved(input_node, l.input_id, l.input_element); + + l.group->add_input_passthrough(resolved); + } + } + + for (auto it = xml_node_data.group_output_links.cbegin(); + it != xml_node_data.group_output_links.cend(); it++) { + auto out_it = xml_node_data.node_ptrs.find(it->second); + if (out_it != xml_node_data.node_ptrs.end()) { + it->first->set_output_passthrough(out_it->second); + } + } +} + +void ProjectSerializer210907::load_node_custom(XmlStreamReader *reader, + Node *node, + XMLNodeData &xml_node_data) const +{ + // Viewer-based nodes + if (ViewerOutput *viewer = dynamic_cast(node)) { + Footage *footage = dynamic_cast(node); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "points") { + load_timeline_points(reader, viewer); + } else if (reader->name() == "timestamp" && footage) { + footage->set_timestamp(strtoll( + reader->read_element_text().c_str(), nullptr, 10)); + } else { + reader->skip_current_element(); + } + } + + } else if (Track *track = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "height") { + track->set_track_height( + strtod(reader->read_element_text().c_str(), nullptr)); + } else { + reader->skip_current_element(); + } + } + + } else if (NodeGroup *group = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthroughs") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthrough") { + XMLNodeData::GroupLink link; + + link.group = group; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + link.input_node = strtoull( + reader->read_element_text().c_str(), + nullptr, 10); + } else if (reader->name() == "input") { + link.input_id = reader->read_element_text(); + } else if (reader->name() == "element") { + link.input_element = atoi( + reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.group_input_links.push_back(link); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "outputpassthrough") { + xml_node_data.group_output_links.insert( + { group, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + + } else { + reader->skip_current_element(); + } +} + +void ProjectSerializer210907::load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "markers") { + load_marker_list(reader, points->get_markers()); + } else if (reader->name() == "workarea") { + load_work_area(reader, points->get_work_area()); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer210907::load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const +{ + Rational range_in = workarea->in(); + Rational range_out = workarea->out(); + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "enabled") { + workarea->set_enabled(attr.value != "0"); + } else if (attr.name == "in") { + range_in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + range_out = Rational::from_string(attr.value); + } + } + + TimeRange loaded_workarea(range_in, range_out); + + if (loaded_workarea != workarea->range()) { + workarea->set_range(loaded_workarea); + } + + reader->skip_current_element(); +} + +void ProjectSerializer210907::load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + std::string name; + Rational in, out; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "name") { + name = attr.value; + } else if (attr.name == "in") { + in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + out = Rational::from_string(attr.value); + } + } + + // ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker + // is de-Qt'd in M4 (must accept std::string name) + new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(), + TimeRange(in, out), name, markers); + } + + reader->skip_current_element(); + } +} + +void ProjectSerializer210907::load_value_hint(Node::ValueHint *hint, + XmlStreamReader *reader) const +{ + std::vector types; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "types") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "type") { + types.push_back(static_cast( + atoi(reader->read_element_text().c_str()))); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "index") { + hint->set_index(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "tag") { + hint->set_tag(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + hint->set_type(types); +} + +} diff --git a/src/node/src/project/serializer/serializer210907.h b/src/node/src/project/serializer/serializer210907.h new file mode 100644 index 000000000..e09250f77 --- /dev/null +++ b/src/node/src/project/serializer/serializer210907.h @@ -0,0 +1,105 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SERIALIZER210907_H +#define OAK_SERIALIZER210907_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer210907 : public ProjectSerializer { +public: + ProjectSerializer210907() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual uint version() const override + { + return 210907; + } + +private: + struct XMLNodeData { + struct SerializedConnection { + NodeInput input; + uintptr_t output_node; + }; + + struct BlockLink { + Node *block; + uintptr_t link; + }; + + struct GroupLink { + NodeGroup *group; + uintptr_t input_node; + std::string input_id; + int input_element; + }; + + std::map node_ptrs; + std::vector desired_connections; + std::vector block_links; + std::vector group_input_links; + std::map group_output_links; + }; + + void load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const; + + void load_color_manager(XmlStreamReader *reader, Project *project) const; + + void load_project_settings(XmlStreamReader *reader, Project *project) const; + + void load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const; + + void load_immediate(XmlStreamReader *reader, Node *node, + const std::string &input, int element, + XMLNodeData &xml_node_data) const; + + bool load_position(XmlStreamReader *reader, uintptr_t *node_ptr, + Node::Position *pos) const; + + void post_connect(const XMLNodeData &xml_node_data) const; + + void load_node_custom(XmlStreamReader *reader, Node *node, + XMLNodeData &xml_node_data) const; + + void load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const; + + void load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const; + + void load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const; + + void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; +}; + +} + +#endif // SERIALIZER211228_H diff --git a/src/node/src/project/serializer/serializer211228.cpp b/src/node/src/project/serializer/serializer211228.cpp new file mode 100644 index 000000000..83af856d8 --- /dev/null +++ b/src/node/src/project/serializer/serializer211228.cpp @@ -0,0 +1,919 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer211228.h" + +#include +#include + +#include "config/config.h" +#include "factory.h" +#include "group/group.h" + +namespace olive +{ + +ProjectSerializer211228::LoadData +ProjectSerializer211228::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + std::map> properties; + std::map> positions; + XMLNodeData xml_node_data; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "uuid") { + project->set_uuid(reader->read_element_text()); + + } else if (reader->name() == "nodes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + std::string id; + + { + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } else if (attr.name == "root" && + attr.value == "1") { + is_root = true; + } else if (attr.name == "cm" && attr.value == "1") { + is_cm = true; + } else if (attr.name == "settings" && + attr.value == "1") { + is_settings = true; + } + } + } + + if (id.empty()) { + fprintf(stderr, "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + Node *node; + bool handled_elsewhere = false; + + if (is_root) { + project->initialize(); + node = project->root(); + } else if (is_cm) { + load_color_manager(reader, project); + handled_elsewhere = true; + } else if (is_settings) { + load_project_settings(reader, project); + handled_elsewhere = true; + } else { + node = NodeFactory::create_from_id(id); + } + + if (!handled_elsewhere) { + if (!node) { + fprintf(stderr, + "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + load_node(node, xml_node_data, reader); + project->add_node(node); + } + } + } + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "positions") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "context") { + uintptr_t context_ptr = 0; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + context_ptr = + strtoull(attr.value.c_str(), nullptr, 10); + break; + } + } + + if (context_ptr) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t node_ptr; + Node::Position node_pos; + + if (load_position(reader, &node_ptr, + &node_pos)) { + if (node_ptr) { + positions[context_ptr].insert( + { node_ptr, node_pos }); + } else { + fprintf(stderr, + "Failed to find pointer for " + "node position\n"); + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + fprintf(stderr, + "Attempted to load context with no pointer\n"); + reader->skip_current_element(); + } + + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "properties") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t ptr = 0; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + ptr = strtoull(attr.value.c_str(), nullptr, 10); + + // Only attribute we're looking for right now + break; + } + } + + if (ptr) { + std::map + properties_for_node; + while (xml_read_next_start_element(reader)) { + properties_for_node.insert( + { std::string(reader->name()), + reader->read_element_text() }); + } + properties.insert({ ptr, properties_for_node }); + } + } else { + reader->skip_current_element(); + } + } + + } else { + // Skip this + reader->skip_current_element(); + } + } + + // Resolve positions + for (auto it = positions.cbegin(); it != positions.cend(); it++) { + auto ctx_it = xml_node_data.node_ptrs.find(it->first); + Node *ctx = ctx_it != xml_node_data.node_ptrs.end() ? ctx_it->second : + nullptr; + if (ctx) { + for (auto jt = it->second.cbegin(); jt != it->second.cend(); jt++) { + auto n_it = xml_node_data.node_ptrs.find(jt->first); + Node *n = n_it != xml_node_data.node_ptrs.end() ? + n_it->second : + nullptr; + if (n) { + ctx->set_node_position_in_context(n, jt->second); + } + } + } + } + + // Make connections + post_connect(xml_node_data); + + LoadData load_data; + load_data.node_ptrs = xml_node_data.node_ptrs; + load_data.node_uuids = xml_node_data.node_uuids; + + // Resolve serialized properties (if any) + for (auto it = properties.cbegin(); it != properties.cend(); it++) { + auto n_it = xml_node_data.node_ptrs.find(it->first); + Node *node = + n_it != xml_node_data.node_ptrs.end() ? n_it->second : nullptr; + if (node) { + load_data.properties.insert({ node, it->second }); + } + } + + // Resolve tracks + for (Node *n : project->nodes()) { + n->set_caches_enabled(true); + + if (Track *t = dynamic_cast(n)) { + for (int i = 0; i < t->input_array_size(Track::k_block_input); i++) { + Block *b = static_cast( + t->get_connected_output(Track::k_block_input, i)); + if (!b->track()) { + t->append_block(b); + } + } + } + } + + return load_data; +} + +void ProjectSerializer211228::load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const +{ + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "input") { + load_input(node, reader, xml_node_data); + } else if (reader->name() == "ptr") { + uintptr_t ptr = + strtoull(reader->read_element_text().c_str(), nullptr, 10); + xml_node_data.node_ptrs.insert({ ptr, node }); + } else if (reader->name() == "label") { + node->set_label(reader->read_element_text()); + } else if (reader->name() == "uuid") { + xml_node_data.node_uuids.insert( + { node, reader->read_element_text() }); + } else if (reader->name() == "color") { + node->set_override_color( + atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "links") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "link") { + xml_node_data.block_links.push_back( + { node, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "custom") { + load_node_custom(reader, node, xml_node_data); + } else if (reader->name() == "connections") { + // Load connections + while (xml_read_next_start_element(reader)) { + if (reader->name() == "connection") { + std::string param_id; + int ele = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "element") { + ele = atoi(attr.value.c_str()); + } else if (attr.name == "input") { + param_id = attr.value; + } + } + + std::string output_node_id; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "output") { + output_node_id = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.desired_connections.push_back( + { NodeInput(node, param_id, ele), + strtoull(output_node_id.c_str(), nullptr, 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "hints") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "hint") { + std::string input; + int element = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "input") { + input = attr.value; + } else if (attr.name == "element") { + element = atoi(attr.value.c_str()); + } + } + + Node::ValueHint vh; + load_value_hint(&vh, reader); + node->set_value_hint_for_input(input, vh, element); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + node->LoadFinishedEvent(); +} + +void ProjectSerializer211228::load_color_manager(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "config" || id == "default_input" || + id == "reference_space") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "default_input") { + // Default color space + // NOTE: Stupidly, we saved these as integers which means we can't add anything to the OCIO + // config. So we must convert back to string here. + static const std::vector list = { + "Linear", + "CIE-XYZ D65", + "Filmic Log Encoding", + "sRGB OETF", + "Apple DCI-P3 D65", + "AppleP3 sRGB OETF", + "BT.1886 EOTF", + "AppleP3 Filmic Log Encoding", + "BT.1886 Filmic Log Encoding", + "Fuji F-Log OETF", + "Fuji F-Log F-Gamut", + "Panasonic V-Log V-Gamut", + "Arri Wide Gamut / LogC EI 800", + "Arri Wide Gamut / LogC EI 400", + "Blackmagic Film Wide Gamut (Gen 5)", + "Rec.709 OETF", + "Non-Colour Data" + }; + int num_value = atoi(value.c_str()); + value = list.at(num_value); + project->set_default_input_color_space(value); + } else if (id == "reference_space") { + // Reference space + if (value == "1") { + value = ocio::ROLE_COMPOSITING_LOG; + } else { + value = ocio::ROLE_SCENE_LINEAR; + } + project->set_color_reference_space(value); + } else { + // Config filename + project->set_color_config_filename(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer211228::load_project_settings(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "cache_setting" || id == "cache_path") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "cache_setting") { + project->set_cache_location_setting( + static_cast( + atoi(value.c_str()))); + } else { + project->set_custom_cache_path(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer211228::load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const +{ + std::string param_id; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + param_id = attr.value; + + break; + } + } + + if (param_id.empty()) { + fprintf(stderr, "Failed to load parameter with missing ID\n"); + reader->skip_current_element(); + return; + } + + if (!node->has_input_with_id(param_id)) { + fprintf(stderr, "Failed to load parameter that didn't exist: %s\n", + param_id.c_str()); + reader->skip_current_element(); + return; + } + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "primary") { + // Load primary immediate + load_immediate(reader, node, param_id, -1, xml_node_data); + } else if (reader->name() == "subelements") { + // Load subelements + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "count") { + node->input_array_resize(param_id, + atoi(attr.value.c_str())); + } + } + + int element_counter = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + load_immediate(reader, node, param_id, element_counter, + xml_node_data); + + element_counter++; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer211228::load_immediate(XmlStreamReader *reader, + Node *node, + const std::string &input, + int element, + XMLNodeData &xml_node_data) const +{ + (void) xml_node_data; + + NodeValue::Type data_type = node->get_input_data_type(input); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + // Load standard value + int val_index = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + Variant value_on_track; + + if (data_type == NodeValue::k_video_params) { + VideoParams vp; + vp.load(reader); + value_on_track = Variant::from_value(vp); + } else if (data_type == NodeValue::k_audio_params) { + AudioParams ap = + TypeSerializer::load_audio_params(reader); + value_on_track = Variant::from_value(ap); + } else { + std::string value_text = reader->read_element_text(); + + if (!value_text.empty()) { + value_on_track = NodeValue::string_to_value( + data_type, value_text, true); + } + } + + node->set_split_standard_value_on_track(input, val_index, + value_on_track, element); + + val_index++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "keyframing" && + node->is_input_keyframable(input)) { + node->set_input_is_keyframing( + input, atoi(reader->read_element_text().c_str()), element); + } else if (reader->name() == "keyframes") { + int track = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "key") { + std::string key_input; + Rational key_time; + NodeKeyframe::Type key_type = NodeKeyframe::k_linear; + Variant key_value; + PointF key_in_handle; + PointF key_out_handle; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (is_cancelled()) { + return; + } + + if (attr.name == "input") { + key_input = attr.value; + } else if (attr.name == "time") { + key_time = + Rational::from_string(attr.value); + } else if (attr.name == "type") { + key_type = static_cast( + atoi(attr.value.c_str())); + } else if (attr.name == "inhandlex") { + key_in_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "inhandley") { + key_in_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandlex") { + key_out_handle.set_x( + strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandley") { + key_out_handle.set_y( + strtod(attr.value.c_str(), nullptr)); + } + } + + key_value = NodeValue::string_to_value( + data_type, reader->read_element_text(), true); + + NodeKeyframe *key = new NodeKeyframe( + key_time, key_value, key_type, track, element, + key_input, node); + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); + + // Replaces the former QObject setParent() (which + // registered the keyframe through childEvent) + node->add_keyframe(key); + } else { + reader->skip_current_element(); + } + } + + track++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "csinput") { + node->set_input_property(input, "col_input", + reader->read_element_text()); + } else if (reader->name() == "csdisplay") { + node->set_input_property(input, "col_display", + reader->read_element_text()); + } else if (reader->name() == "csview") { + node->set_input_property(input, "col_view", + reader->read_element_text()); + } else if (reader->name() == "cslook") { + node->set_input_property(input, "col_look", + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } +} + +bool ProjectSerializer211228::load_position(XmlStreamReader *reader, + uintptr_t *node_ptr, + Node::Position *pos) const +{ + bool got_node_ptr = false; + bool got_pos_x = false; + bool got_pos_y = false; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + *node_ptr = strtoull(attr.value.c_str(), nullptr, 10); + got_node_ptr = true; + break; + } + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "x") { + pos->position.set_x(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_x = true; + } else if (reader->name() == "y") { + pos->position.set_y(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_y = true; + } else if (reader->name() == "expanded") { + pos->expanded = atoi(reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + return got_node_ptr && got_pos_x && got_pos_y; +} + +void ProjectSerializer211228::post_connect(const XMLNodeData &xml_node_data) const +{ + for (const XMLNodeData::SerializedConnection &con : + xml_node_data.desired_connections) { + auto out_it = xml_node_data.node_ptrs.find(con.output_node); + if (out_it != xml_node_data.node_ptrs.end()) { + Node::connect_edge(out_it->second, con.input); + } + } + + for (const XMLNodeData::BlockLink &l : xml_node_data.block_links) { + Node *a = l.block; + auto b_it = xml_node_data.node_ptrs.find(l.link); + Node *b = + b_it != xml_node_data.node_ptrs.end() ? b_it->second : nullptr; + + Node::link(a, b); + } + + for (const XMLNodeData::GroupLink &l : xml_node_data.group_input_links) { + auto in_it = xml_node_data.node_ptrs.find(l.input_node); + if (in_it != xml_node_data.node_ptrs.end()) { + Node *input_node = in_it->second; + NodeInput resolved(input_node, l.input_id, l.input_element); + + l.group->add_input_passthrough(resolved); + } + } + + for (auto it = xml_node_data.group_output_links.cbegin(); + it != xml_node_data.group_output_links.cend(); it++) { + auto out_it = xml_node_data.node_ptrs.find(it->second); + if (out_it != xml_node_data.node_ptrs.end()) { + it->first->set_output_passthrough(out_it->second); + } + } +} + +void ProjectSerializer211228::load_node_custom(XmlStreamReader *reader, + Node *node, + XMLNodeData &xml_node_data) const +{ + // Viewer-based nodes + if (ViewerOutput *viewer = dynamic_cast(node)) { + Footage *footage = dynamic_cast(node); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "points") { + load_timeline_points(reader, viewer); + } else if (reader->name() == "timestamp" && footage) { + footage->set_timestamp(strtoll( + reader->read_element_text().c_str(), nullptr, 10)); + } else { + reader->skip_current_element(); + } + } + + } else if (Track *track = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "height") { + track->set_track_height( + strtod(reader->read_element_text().c_str(), nullptr)); + } else { + reader->skip_current_element(); + } + } + + } else if (NodeGroup *group = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthroughs") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthrough") { + XMLNodeData::GroupLink link; + + link.group = group; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + link.input_node = strtoull( + reader->read_element_text().c_str(), + nullptr, 10); + } else if (reader->name() == "input") { + link.input_id = reader->read_element_text(); + } else if (reader->name() == "element") { + link.input_element = atoi( + reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.group_input_links.push_back(link); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "outputpassthrough") { + xml_node_data.group_output_links.insert( + { group, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + + } else { + reader->skip_current_element(); + } +} + +void ProjectSerializer211228::load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "markers") { + load_marker_list(reader, points->get_markers()); + } else if (reader->name() == "workarea") { + load_work_area(reader, points->get_work_area()); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer211228::load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const +{ + Rational range_in = workarea->in(); + Rational range_out = workarea->out(); + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "enabled") { + workarea->set_enabled(attr.value != "0"); + } else if (attr.name == "in") { + range_in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + range_out = Rational::from_string(attr.value); + } + } + + TimeRange loaded_workarea(range_in, range_out); + + if (loaded_workarea != workarea->range()) { + workarea->set_range(loaded_workarea); + } + + reader->skip_current_element(); +} + +void ProjectSerializer211228::load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + std::string name; + Rational in, out; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "name") { + name = attr.value; + } else if (attr.name == "in") { + in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + out = Rational::from_string(attr.value); + } + } + + // ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker + // is de-Qt'd in M4 (must accept std::string name) + new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(), + TimeRange(in, out), name, markers); + } + + reader->skip_current_element(); + } +} + +void ProjectSerializer211228::load_value_hint(Node::ValueHint *hint, + XmlStreamReader *reader) const +{ + std::vector types; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "types") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "type") { + types.push_back(static_cast( + atoi(reader->read_element_text().c_str()))); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "index") { + hint->set_index(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "tag") { + hint->set_tag(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + hint->set_type(types); +} + +} diff --git a/src/node/src/project/serializer/serializer211228.h b/src/node/src/project/serializer/serializer211228.h new file mode 100644 index 000000000..8dd92f8a2 --- /dev/null +++ b/src/node/src/project/serializer/serializer211228.h @@ -0,0 +1,106 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SERIALIZER211228_H +#define OAK_SERIALIZER211228_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer211228 : public ProjectSerializer { +public: + ProjectSerializer211228() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual uint version() const override + { + return 211228; + } + +private: + struct XMLNodeData { + struct SerializedConnection { + NodeInput input; + uintptr_t output_node; + }; + + struct BlockLink { + Node *block; + uintptr_t link; + }; + + struct GroupLink { + NodeGroup *group; + uintptr_t input_node; + std::string input_id; + int input_element; + }; + + std::map node_ptrs; + std::vector desired_connections; + std::vector block_links; + std::vector group_input_links; + std::map group_output_links; + std::map node_uuids; + }; + + void load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const; + + void load_color_manager(XmlStreamReader *reader, Project *project) const; + + void load_project_settings(XmlStreamReader *reader, Project *project) const; + + void load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const; + + void load_immediate(XmlStreamReader *reader, Node *node, + const std::string &input, int element, + XMLNodeData &xml_node_data) const; + + bool load_position(XmlStreamReader *reader, uintptr_t *node_ptr, + Node::Position *pos) const; + + void post_connect(const XMLNodeData &xml_node_data) const; + + void load_node_custom(XmlStreamReader *reader, Node *node, + XMLNodeData &xml_node_data) const; + + void load_timeline_points(XmlStreamReader *reader, + ViewerOutput *points) const; + + void load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const; + + void load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const; + + void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; +}; + +} + +#endif // OAK_SERIALIZER211228_H diff --git a/src/node/src/project/serializer/serializer220403.cpp b/src/node/src/project/serializer/serializer220403.cpp new file mode 100644 index 000000000..1c505abf2 --- /dev/null +++ b/src/node/src/project/serializer/serializer220403.cpp @@ -0,0 +1,1203 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializer220403.h" + +#include +#include + +#include "config/config.h" +#include "factory.h" +#include "block/clip/clip.h" +#include "group/group.h" + +namespace olive +{ + +ProjectSerializer220403::LoadData +ProjectSerializer220403::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + std::map> properties; + std::map> positions; + XMLNodeData xml_node_data; + + LoadData load_data; + + if ((load_type == k_project && reader->name() == "project") || + ((load_type == k_only_nodes && reader->name() == "nodes") || + (load_type == k_only_clips && reader->name() == "timeline")) || + (load_type == k_only_keyframes && reader->name() == "keyframes") || + (load_type == k_only_markers && reader->name() == "markers")) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "layout") { + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + load_data.layout = SerializedLayoutInfo::from_xml( + reader, xml_node_data.node_ptrs); + + } else if (reader->name() == "uuid") { + if (project) { + project->set_uuid(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + + } else if (reader->name() == "nodes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + std::string id; + + { + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } else if (attr.name == "root" && + attr.value == "1") { + is_root = true; + } else if (attr.name == "cm" && + attr.value == "1") { + is_cm = true; + } else if (attr.name == "settings" && + attr.value == "1") { + is_settings = true; + } + } + } + + if (id.empty()) { + fprintf(stderr, + "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + Node *node; + bool handled_elsewhere = false; + + if (is_root) { + project->initialize(); + node = project->root(); + } else if (is_cm) { + load_color_manager(reader, project); + handled_elsewhere = true; + } else if (is_settings) { + load_project_settings(reader, project); + handled_elsewhere = true; + } else { + node = NodeFactory::create_from_id(id); + } + + if (!handled_elsewhere) { + if (!node) { + fprintf(stderr, + "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + load_node(node, xml_node_data, reader); + if (project) { + project->add_node(node); + } else { + load_data.nodes.push_back(node); + } + } + } + } + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "keyframes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + std::string node_id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + node_id = attr.value; + break; + } + } + + Node *n = nullptr; + if (!node_id.empty()) { + n = NodeFactory::create_from_id(node_id); + } + + if (!n) { + reader->skip_current_element(); + } else { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string input_id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + input_id = attr.value; + break; + } + } + + if (input_id.empty()) { + reader->skip_current_element(); + } else { + while ( + xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + std::string element_id; + for (const XmlStreamAttribute + &attr : + reader->attributes()) { + if (attr.name == "id") { + element_id = attr.value; + break; + } + } + + if (element_id.empty()) { + reader + ->skip_current_element(); + } else { + while ( + xml_read_next_start_element( + reader)) { + if (reader->name() == + "track") { + std::string + track_id; + for (const XmlStreamAttribute + &attr : + reader->attributes()) { + if (attr.name == + "id") { + track_id = + attr.value; + break; + } + } + + if (track_id + .empty()) { + reader + ->skip_current_element(); + } else { + while ( + xml_read_next_start_element( + reader)) { + if (reader + ->name() == + "key") { + NodeKeyframe + *key = + new NodeKeyframe(); + key->set_input( + input_id); + key->set_element( + atoi(element_id + .c_str())); + key->set_track( + atoi(track_id + .c_str())); + + load_keyframe( + reader, + key, + n->get_input_data_type( + input_id)); + + load_data + .keyframes + [node_id] + .push_back( + key); + } else { + reader + ->skip_current_element(); + } + } + } + } else { + reader + ->skip_current_element(); + } + } + } + } else { + reader->skip_current_element(); + } + } + } + } else { + reader->skip_current_element(); + } + } + } + + delete n; + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "markers") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + TimelineMarker *marker = new TimelineMarker(); + load_marker(reader, marker); + load_data.markers.push_back(marker); + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "positions") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "context") { + uintptr_t context_ptr = 0; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + context_ptr = + strtoull(attr.value.c_str(), nullptr, 10); + break; + } + } + + if (context_ptr) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t node_ptr; + Node::Position node_pos; + + if (load_position(reader, &node_ptr, + &node_pos)) { + if (node_ptr) { + positions[context_ptr].insert( + { node_ptr, node_pos }); + } else { + fprintf(stderr, + "Failed to find pointer " + "for node position\n"); + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + fprintf(stderr, + "Attempted to load context with no " + "pointer\n"); + reader->skip_current_element(); + } + + } else { + reader->skip_current_element(); + } + } + + } else if (reader->name() == "properties") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t ptr = 0; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + ptr = + strtoull(attr.value.c_str(), nullptr, 10); + + // Only attribute we're looking for right now + break; + } + } + + if (ptr) { + std::map + properties_for_node; + while (xml_read_next_start_element(reader)) { + properties_for_node.insert( + { std::string(reader->name()), + reader->read_element_text() }); + } + properties.insert({ ptr, properties_for_node }); + } + } else { + reader->skip_current_element(); + } + } + + } else { + // Skip this + reader->skip_current_element(); + } + } + } + + // Resolve positions + for (auto it = positions.cbegin(); it != positions.cend(); it++) { + auto ctx_it = xml_node_data.node_ptrs.find(it->first); + Node *ctx = ctx_it != xml_node_data.node_ptrs.end() ? ctx_it->second : + nullptr; + if (ctx) { + for (auto jt = it->second.cbegin(); jt != it->second.cend(); jt++) { + auto n_it = xml_node_data.node_ptrs.find(jt->first); + Node *n = n_it != xml_node_data.node_ptrs.end() ? + n_it->second : + nullptr; + if (n) { + ctx->set_node_position_in_context(n, jt->second); + } + } + } + } + + // Make connections + post_connect(xml_node_data); + + load_data.node_ptrs = xml_node_data.node_ptrs; + load_data.node_uuids = xml_node_data.node_uuids; + + // Resolve serialized properties (if any) + for (auto it = properties.cbegin(); it != properties.cend(); it++) { + auto n_it = xml_node_data.node_ptrs.find(it->first); + Node *node = + n_it != xml_node_data.node_ptrs.end() ? n_it->second : nullptr; + if (node) { + load_data.properties.insert({ node, it->second }); + } + } + + // Re-enable caches and resolve tracks + const std::vector &nodes = + project ? project->nodes() : load_data.nodes; + for (Node *n : nodes) { + n->set_caches_enabled(true); + + if (Track *t = dynamic_cast(n)) { + for (int i = 0; i < t->input_array_size(Track::k_block_input); i++) { + Block *b = static_cast( + t->get_connected_output(Track::k_block_input, i)); + if (!b->track()) { + t->append_block(b); + } + } + } + + // Clear duplicate label (to facilitate #2147) + if (ClipBlock *c = dynamic_cast(n)) { + if (c->connected_viewer() && + c->get_label() == c->connected_viewer()->get_label()) { + c->set_label(std::string()); + } + } + } + + return load_data; +} + +void ProjectSerializer220403::load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const +{ + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "input") { + load_input(node, reader, xml_node_data); + } else if (reader->name() == "ptr") { + uintptr_t ptr = + strtoull(reader->read_element_text().c_str(), nullptr, 10); + xml_node_data.node_ptrs.insert({ ptr, node }); + } else if (reader->name() == "label") { + node->set_label(reader->read_element_text()); + } else if (reader->name() == "uuid") { + xml_node_data.node_uuids.insert( + { node, reader->read_element_text() }); + } else if (reader->name() == "color") { + node->set_override_color( + atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "links") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "link") { + xml_node_data.block_links.push_back( + { node, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "custom") { + load_node_custom(reader, node, xml_node_data); + } else if (reader->name() == "connections") { + // Load connections + while (xml_read_next_start_element(reader)) { + if (reader->name() == "connection") { + std::string param_id; + int ele = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "element") { + ele = atoi(attr.value.c_str()); + } else if (attr.name == "input") { + param_id = attr.value; + } + } + + std::string output_node_id; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "output") { + output_node_id = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + + xml_node_data.desired_connections.push_back( + { NodeInput(node, param_id, ele), + strtoull(output_node_id.c_str(), nullptr, 10) }); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "hints") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "hint") { + std::string input; + int element = -1; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "input") { + input = attr.value; + } else if (attr.name == "element") { + element = atoi(attr.value.c_str()); + } + } + + Node::ValueHint vh; + load_value_hint(&vh, reader); + node->set_value_hint_for_input(input, vh, element); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "caches") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "audio") { + node->audio_playback_cache()->set_uuid( + reader->read_element_text()); + } else if (reader->name() == "video") { + node->video_frame_cache()->set_uuid( + reader->read_element_text()); + } else if (reader->name() == "thumb") { + node->thumbnail_cache()->set_uuid( + reader->read_element_text()); + } else if (reader->name() == "waveform") { + node->waveform_cache()->set_uuid( + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + node->LoadFinishedEvent(); +} + +void ProjectSerializer220403::load_color_manager(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "config" || id == "default_input" || + id == "reference_space") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "default_input") { + // Default color space + // NOTE: Stupidly, we saved these as integers which means we can't add anything to the OCIO + // config. So we must convert back to string here. + static const std::vector list = { + "Linear", + "Filmic Log Encoding", + "sRGB OETF", + "Apple DCI-P3 D65", + "AppleP3 sRGB OETF", + "BT.1886 EOTF", + "AppleP3 Filmic Log Encoding", + "BT.1886 Filmic Log Encoding", + "Fuji F-Log OETF", + "Fuji F-Log F-Gamut", + "Panasonic V-Log V-Gamut", + "Arri Wide Gamut / LogC EI 800", + "Arri Wide Gamut / LogC EI 400", + "Blackmagic Film Wide Gamut (Gen 5)", + "Rec.709 OETF", + "Non-Colour Data" + }; + int num_value = atoi(value.c_str()); + value = list.at(num_value); + project->set_default_input_color_space(value); + } else if (id == "reference_space") { + // Reference space + if (value == "1") { + value = ocio::ROLE_COMPOSITING_LOG; + } else { + value = ocio::ROLE_SCENE_LINEAR; + } + project->set_color_reference_space(value); + } else { + // Config filename + project->set_color_config_filename(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_project_settings(XmlStreamReader *reader, + Project *project) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string id; + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } + } + + if (id == "cache_setting" || id == "cache_path") { + std::string value; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "primary") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "track") { + value = reader->read_element_text(); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (id == "cache_setting") { + project->set_cache_location_setting( + static_cast( + atoi(value.c_str()))); + } else { + project->set_custom_cache_path(value); + } + } else { + reader->skip_current_element(); + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const +{ + if (dynamic_cast(node)) { + // Ignore input of group + reader->skip_current_element(); + return; + } + + std::string param_id; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "id") { + param_id = attr.value; + + break; + } + } + + if (param_id.empty()) { + fprintf(stderr, "Failed to load parameter with missing ID\n"); + reader->skip_current_element(); + return; + } + + if (!node->has_input_with_id(param_id)) { + fprintf(stderr, "Failed to load parameter that didn't exist: %s\n", + param_id.c_str()); + reader->skip_current_element(); + return; + } + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "primary") { + // Load primary immediate + load_immediate(reader, node, param_id, -1, xml_node_data); + } else if (reader->name() == "subelements") { + // Load subelements + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "count") { + node->input_array_resize(param_id, + atoi(attr.value.c_str())); + } + } + + int element_counter = 0; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + load_immediate(reader, node, param_id, element_counter, + xml_node_data); + + element_counter++; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_immediate(XmlStreamReader *reader, + Node *node, + const std::string &input, + int element, + XMLNodeData &xml_node_data) const +{ + (void) xml_node_data; + + NodeValue::Type data_type = node->get_input_data_type(input); + + // HACK: SubtitleParams contain the actual subtitle data, so loading/replacing it will overwrite + // the valid subtitles. We hack around it by simply skipping loading subtitles, we'll see + // if this ends up being an issue in the future. + if (data_type == NodeValue::k_subtitle_params) { + reader->skip_current_element(); + return; + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "standard") { + // Load standard value + int val_index = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + Variant value_on_track; + + if (data_type == NodeValue::k_video_params) { + VideoParams vp; + vp.load(reader); + value_on_track = Variant::from_value(vp); + } else if (data_type == NodeValue::k_audio_params) { + AudioParams ap = + TypeSerializer::load_audio_params(reader); + value_on_track = Variant::from_value(ap); + } else { + std::string value_text = reader->read_element_text(); + + if (!value_text.empty()) { + value_on_track = NodeValue::string_to_value( + data_type, value_text, true); + } + } + + node->set_split_standard_value_on_track(input, val_index, + value_on_track, element); + + val_index++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "keyframing" && + node->is_input_keyframable(input)) { + node->set_input_is_keyframing( + input, atoi(reader->read_element_text().c_str()), element); + } else if (reader->name() == "keyframes") { + int track = 0; + + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "track") { + while (xml_read_next_start_element(reader)) { + if (is_cancelled()) { + return; + } + + if (reader->name() == "key") { + NodeKeyframe *key = new NodeKeyframe(); + key->set_input(input); + key->set_element(element); + key->set_track(track); + + load_keyframe(reader, key, data_type); + + // Replaces the former QObject setParent() (which + // registered the keyframe through childEvent) + node->add_keyframe(key); + } else { + reader->skip_current_element(); + } + } + + track++; + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "csinput") { + node->set_input_property(input, "col_input", + reader->read_element_text()); + } else if (reader->name() == "csdisplay") { + node->set_input_property(input, "col_display", + reader->read_element_text()); + } else if (reader->name() == "csview") { + node->set_input_property(input, "col_view", + reader->read_element_text()); + } else if (reader->name() == "cslook") { + node->set_input_property(input, "col_look", + reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_keyframe(XmlStreamReader *reader, + NodeKeyframe *key, + NodeValue::Type data_type) const +{ + std::string key_input; + PointF key_in_handle; + PointF key_out_handle; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (is_cancelled()) { + return; + } + + if (attr.name == "input") { + key_input = attr.value; + } else if (attr.name == "time") { + key->set_time(Rational::from_string(attr.value)); + } else if (attr.name == "type") { + key->set_type_no_bezier_adj(static_cast( + atoi(attr.value.c_str()))); + } else if (attr.name == "inhandlex") { + key_in_handle.set_x(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "inhandley") { + key_in_handle.set_y(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandlex") { + key_out_handle.set_x(strtod(attr.value.c_str(), nullptr)); + } else if (attr.name == "outhandley") { + key_out_handle.set_y(strtod(attr.value.c_str(), nullptr)); + } + } + + key->set_value( + NodeValue::string_to_value(data_type, reader->read_element_text(), true)); + + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); +} + +bool ProjectSerializer220403::load_position(XmlStreamReader *reader, + uintptr_t *node_ptr, + Node::Position *pos) const +{ + bool got_node_ptr = false; + bool got_pos_x = false; + bool got_pos_y = false; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "ptr") { + *node_ptr = strtoull(attr.value.c_str(), nullptr, 10); + got_node_ptr = true; + break; + } + } + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "x") { + pos->position.set_x(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_x = true; + } else if (reader->name() == "y") { + pos->position.set_y(strtod(reader->read_element_text().c_str(), + nullptr)); + got_pos_y = true; + } else if (reader->name() == "expanded") { + pos->expanded = atoi(reader->read_element_text().c_str()); + } else { + reader->skip_current_element(); + } + } + + return got_node_ptr && got_pos_x && got_pos_y; +} + +void ProjectSerializer220403::post_connect(const XMLNodeData &xml_node_data) const +{ + for (const XMLNodeData::SerializedConnection &con : + xml_node_data.desired_connections) { + auto out_it = xml_node_data.node_ptrs.find(con.output_node); + if (out_it != xml_node_data.node_ptrs.end()) { + Node::connect_edge(out_it->second, con.input); + } + } + + for (const XMLNodeData::BlockLink &l : xml_node_data.block_links) { + Node *a = l.block; + auto b_it = xml_node_data.node_ptrs.find(l.link); + Node *b = + b_it != xml_node_data.node_ptrs.end() ? b_it->second : nullptr; + + Node::link(a, b); + } + + for (const XMLNodeData::GroupLink &l : xml_node_data.group_input_links) { + auto in_it = xml_node_data.node_ptrs.find(l.input_node); + if (in_it != xml_node_data.node_ptrs.end()) { + Node *input_node = in_it->second; + NodeInput resolved(input_node, l.input_id, l.input_element); + + l.group->add_input_passthrough(resolved, l.passthrough_id); + + l.group->set_input_flag(l.passthrough_id, + InputFlag(l.custom_flags.value())); + + if (!l.custom_name.empty()) { + l.group->set_input_name(l.passthrough_id, l.custom_name); + } + + l.group->set_input_data_type(l.passthrough_id, l.data_type); + + l.group->set_default_value(l.passthrough_id, l.default_val); + + for (auto it = l.custom_properties.cbegin(); + it != l.custom_properties.cend(); it++) { + l.group->set_input_property(l.passthrough_id, it->first, + it->second); + } + } + } + + for (auto it = xml_node_data.group_output_links.cbegin(); + it != xml_node_data.group_output_links.cend(); it++) { + auto out_it = xml_node_data.node_ptrs.find(it->second); + if (out_it != xml_node_data.node_ptrs.end()) { + it->first->set_output_passthrough(out_it->second); + } + } +} + +void ProjectSerializer220403::load_node_custom(XmlStreamReader *reader, + Node *node, + XMLNodeData &xml_node_data) const +{ + // Viewer-based nodes + if (ViewerOutput *viewer = dynamic_cast(node)) { + Footage *footage = dynamic_cast(node); + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "points") { + load_timeline_points(reader, viewer); + } else if (reader->name() == "timestamp" && footage) { + footage->set_timestamp(strtoll( + reader->read_element_text().c_str(), nullptr, 10)); + } else { + reader->skip_current_element(); + } + } + + } else if (Track *track = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "height") { + track->set_track_height( + strtod(reader->read_element_text().c_str(), nullptr)); + } else { + reader->skip_current_element(); + } + } + + } else if (NodeGroup *group = dynamic_cast(node)) { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthroughs") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "inputpassthrough") { + XMLNodeData::GroupLink link; + + link.group = group; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + link.input_node = strtoull( + reader->read_element_text().c_str(), + nullptr, 10); + } else if (reader->name() == "input") { + link.input_id = reader->read_element_text(); + } else if (reader->name() == "element") { + link.input_element = atoi( + reader->read_element_text().c_str()); + } else if (reader->name() == "id") { + link.passthrough_id = + reader->read_element_text(); + } else if (reader->name() == "name") { + link.custom_name = reader->read_element_text(); + } else if (reader->name() == "flags") { + link.custom_flags = InputFlags(strtoull( + reader->read_element_text().c_str(), + nullptr, 10)); + } else if (reader->name() == "type") { + link.data_type = + NodeValue::get_data_type_from_name( + reader->read_element_text()); + } else if (reader->name() == "default") { + link.default_val = NodeValue::string_to_value( + link.data_type, reader->read_element_text(), + false); + } else if (reader->name() == "properties") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "property") { + std::string key; + std::string value; + + while ( + xml_read_next_start_element(reader)) { + if (reader->name() == "key") { + key = + reader->read_element_text(); + } else if (reader->name() == + "value") { + value = + reader->read_element_text(); + } else { + reader + ->skip_current_element(); + } + } + + if (!key.empty()) { + link.custom_properties.insert( + { key, value }); + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + xml_node_data.group_input_links.push_back(link); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "outputpassthrough") { + xml_node_data.group_output_links.insert( + { group, + strtoull(reader->read_element_text().c_str(), nullptr, + 10) }); + } else { + reader->skip_current_element(); + } + } + + } else { + reader->skip_current_element(); + } +} + +void ProjectSerializer220403::load_timeline_points(XmlStreamReader *reader, + ViewerOutput *viewer) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "markers") { + load_marker_list(reader, viewer->get_markers()); + } else if (reader->name() == "workarea") { + load_work_area(reader, viewer->get_work_area()); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_marker(XmlStreamReader *reader, + TimelineMarker *marker) const +{ + Rational in, out; + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "name") { + marker->set_name(attr.value); + } else if (attr.name == "in") { + in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + out = Rational::from_string(attr.value); + } else if (attr.name == "color") { + marker->set_color(atoi(attr.value.c_str())); + } + } + + marker->set_time(TimeRange(in, out)); + + // This element has no inner text, so just skip it + reader->skip_current_element(); +} + +void ProjectSerializer220403::load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const +{ + Rational range_in = workarea->in(); + Rational range_out = workarea->out(); + + for (const XmlStreamAttribute &attr : reader->attributes()) { + if (attr.name == "enabled") { + workarea->set_enabled(attr.value != "0"); + } else if (attr.name == "in") { + range_in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + range_out = Rational::from_string(attr.value); + } + } + + TimeRange loaded_workarea(range_in, range_out); + + if (loaded_workarea != workarea->range()) { + workarea->set_range(loaded_workarea); + } + + reader->skip_current_element(); +} + +void ProjectSerializer220403::load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const +{ + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + TimelineMarker *marker = new TimelineMarker(markers); + load_marker(reader, marker); + } else { + reader->skip_current_element(); + } + } +} + +void ProjectSerializer220403::load_value_hint(Node::ValueHint *hint, + XmlStreamReader *reader) const +{ + std::vector types; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "types") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "type") { + types.push_back(static_cast( + atoi(reader->read_element_text().c_str()))); + } else { + reader->skip_current_element(); + } + } + } else if (reader->name() == "index") { + hint->set_index(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "tag") { + hint->set_tag(reader->read_element_text()); + } else { + reader->skip_current_element(); + } + } + + hint->set_type(types); +} + +} diff --git a/src/node/src/project/serializer/serializer220403.h b/src/node/src/project/serializer/serializer220403.h new file mode 100644 index 000000000..8fa92f51a --- /dev/null +++ b/src/node/src/project/serializer/serializer220403.h @@ -0,0 +1,117 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SERIALIZER220403_H +#define OAK_SERIALIZER220403_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer220403 : public ProjectSerializer { +public: + ProjectSerializer220403() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual uint version() const override + { + return 220403; + } + +private: + struct XMLNodeData { + struct SerializedConnection { + NodeInput input; + uintptr_t output_node; + }; + + struct BlockLink { + Node *block; + uintptr_t link; + }; + + struct GroupLink { + NodeGroup *group; + std::string passthrough_id; + uintptr_t input_node; + std::string input_id; + int input_element; + std::string custom_name; + InputFlags custom_flags; + NodeValue::Type data_type; + Variant default_val; + std::map custom_properties; + }; + + std::map node_ptrs; + std::vector desired_connections; + std::vector block_links; + std::vector group_input_links; + std::map group_output_links; + std::map node_uuids; + }; + + void load_node(Node *node, XMLNodeData &xml_node_data, + XmlStreamReader *reader) const; + + void load_color_manager(XmlStreamReader *reader, Project *project) const; + + void load_project_settings(XmlStreamReader *reader, Project *project) const; + + void load_input(Node *node, XmlStreamReader *reader, + XMLNodeData &xml_node_data) const; + + void load_immediate(XmlStreamReader *reader, Node *node, + const std::string &input, int element, + XMLNodeData &xml_node_data) const; + + void load_keyframe(XmlStreamReader *reader, NodeKeyframe *key, + NodeValue::Type data_type) const; + + bool load_position(XmlStreamReader *reader, uintptr_t *node_ptr, + Node::Position *pos) const; + + void post_connect(const XMLNodeData &xml_node_data) const; + + void load_node_custom(XmlStreamReader *reader, Node *node, + XMLNodeData &xml_node_data) const; + + void load_timeline_points(XmlStreamReader *reader, + ViewerOutput *viewer) const; + + void load_marker(XmlStreamReader *reader, TimelineMarker *marker) const; + + void load_work_area(XmlStreamReader *reader, + TimelineWorkArea *workarea) const; + + void load_marker_list(XmlStreamReader *reader, + TimelineMarkerList *markers) const; + + void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; +}; + +} + +#endif // OAK_SERIALIZER220403_H diff --git a/src/node/src/project/serializer/serializer230220.cpp b/src/node/src/project/serializer/serializer230220.cpp new file mode 100644 index 000000000..c7a49cf8c --- /dev/null +++ b/src/node/src/project/serializer/serializer230220.cpp @@ -0,0 +1,577 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ +#include +#include +#include +#include + +#include "serializer230220.h" + +#include "config/config.h" +#include "factory.h" +#include "group/group.h" +#include "serializeddata.h" +#include "olive/core/util/stringutils.h" + +namespace olive +{ + +ProjectSerializer230220::LoadData +ProjectSerializer230220::load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const +{ + std::map> properties; + std::map> positions; + LoadData load_data; + SerializedData project_data; + + switch (load_type) { + case k_project: { + if (reader->name() == "project") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "project") { + project_data = project->load(reader); + load_data.node_ptrs = project_data.node_ptrs; + } else if (reader->name() == "layout") { + load_data.layout = SerializedLayoutInfo::from_xml( + reader, project_data.node_ptrs); + } else { + reader->skip_current_element(); + } + } + + post_connect(project->nodes(), &project_data); + } else { + reader->skip_current_element(); + } + break; + } + case k_only_markers: { + if (reader->name() == "markers") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "marker") { + TimelineMarker *marker = new TimelineMarker(); + marker->load(reader); + load_data.markers.push_back(marker); + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + break; + } + case k_only_keyframes: { + if (reader->name() == "keyframes") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + std::string node_id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + node_id = attr.value; + break; + } + } + + Node *n = nullptr; + if (!node_id.empty()) { + n = NodeFactory::create_from_id(node_id); + } + + if (!n) { + reader->skip_current_element(); + } else { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "input") { + std::string input_id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + input_id = attr.value; + break; + } + } + + if (input_id.empty()) { + reader->skip_current_element(); + } else { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "element") { + std::string element_id; + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + element_id = attr.value; + break; + } + } + + if (element_id.empty()) { + reader->skip_current_element(); + } else { + while ( + xml_read_next_start_element( + reader)) { + if (reader->name() == + "track") { + std::string track_id; + for (const XmlStreamAttribute + &attr : + reader->attributes()) { + if (attr.name == + "id") { + track_id = + attr.value; + break; + } + } + + if (track_id.empty()) { + reader + ->skip_current_element(); + } else { + while ( + xml_read_next_start_element( + reader)) { + if (reader + ->name() == + "key") { + NodeKeyframe + *key = + new NodeKeyframe(); + key->set_input( + input_id); + key->set_element( + atoi(element_id + .c_str())); + key->set_track( + atoi(track_id + .c_str())); + + key->load( + reader, + n->get_input_data_type( + input_id)); + + load_data + .keyframes + [node_id] + .push_back( + key); + } else { + reader + ->skip_current_element(); + } + } + } + } else { + reader + ->skip_current_element(); + } + } + } + } else { + reader->skip_current_element(); + } + } + } + } else { + reader->skip_current_element(); + } + } + } + + delete n; + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + break; + } + case k_only_clips: + case k_only_nodes: { + if ((load_type == k_only_nodes && reader->name() == "nodes") || + (load_type == k_only_clips && reader->name() == "timeline")) { + std::map skipped_items; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + std::string id; + uintptr_t ptr = 0; + std::vector items; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "id") { + id = attr.value; + } else if (attr.name == "ptr") { + ptr = strtoull(attr.value.c_str(), nullptr, 10); + } else if (attr.name == "items") { + std::vector l = + core::StringUtils::split(attr.value, ','); + items.reserve(l.size()); + for (const std::string &s : l) { + items.push_back( + strtoull(s.c_str(), nullptr, 10)); + } + } + } + + if (id.empty()) { + fprintf(stderr, "Failed to load node with empty ID\n"); + reader->skip_current_element(); + } else { + bool dependency_of_item = false; + + if (project && !items.empty()) { + for (uintptr_t p : items) { + Node *item_node = + reinterpret_cast(p); + if (std::find(project->nodes().begin(), + project->nodes().end(), + item_node) != + project->nodes().end()) { + dependency_of_item = true; + break; + } + } + } + + if (dependency_of_item) { + reader->skip_current_element(); + } else { + Node *node = NodeFactory::create_from_id(id); + if (!node) { + fprintf(stderr, + "Failed to find node with ID %s\n", + id.c_str()); + reader->skip_current_element(); + } else { + if (project && node->is_item() && ptr) { + // If we're pasting an object into the same project, we should re-use the item + // rather than duplicate. + Node *existing = + reinterpret_cast(ptr); + if (std::find(project->nodes().begin(), + project->nodes().end(), + existing) != + project->nodes().end()) { + // Connect this + skipped_items.insert({ ptr, existing }); + + // Don't continue loading this + delete node; + node = nullptr; + + // Skip element + reader->skip_current_element(); + } + } + + if (node) { + // Disable cache while node is being loaded (we'll re-enable it later) + node->set_caches_enabled(false); + node->load(reader, &project_data); + load_data.nodes.push_back(node); + } + } + } + } + + load_data.node_ptrs = project_data.node_ptrs; + } else if (reader->name() == "properties") { + while (xml_read_next_start_element(reader)) { + if (reader->name() == "node") { + uintptr_t ptr = 0; + + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "ptr") { + ptr = strtoull(attr.value.c_str(), nullptr, + 10); + + // Only attribute we're looking for right now + break; + } + } + + if (ptr) { + std::map + properties_for_node; + while (xml_read_next_start_element(reader)) { + properties_for_node.insert( + { std::string(reader->name()), + reader->read_element_text() }); + } + properties.insert({ ptr, properties_for_node }); + } + } else { + reader->skip_current_element(); + } + } + } else { + reader->skip_current_element(); + } + } + + if (!skipped_items.empty()) { + for (auto it = project_data.desired_connections.begin(); + it != project_data.desired_connections.end();) { + const SerializedData::SerializedConnection &sc = *it; + + auto si_it = skipped_items.find(sc.output_node); + if (si_it != skipped_items.end()) { + // Convert this to a promised connection + Node::OutputConnection oc = { si_it->second, sc.input }; + load_data.promised_connections.push_back(oc); + it = project_data.desired_connections.erase(it); + } else { + it++; + } + } + } + + // Resolve serialized properties (if any) + for (auto it = properties.cbegin(); it != properties.cend(); it++) { + auto n_it = project_data.node_ptrs.find(it->first); + Node *node = n_it != project_data.node_ptrs.end() ? + n_it->second : + nullptr; + if (node) { + load_data.properties.insert({ node, it->second }); + } + } + + post_connect(load_data.nodes, &project_data); + } else { + reader->skip_current_element(); + } + break; + } + } + + return load_data; +} + +void write_node_map(XmlStreamWriter *writer, Node *node, + const std::vector &nodes) +{ + writer->write_start_element("node"); + + writer->write_attribute("ptr", + std::to_string(reinterpret_cast(node))); + + for (auto oc : node->output_connections()) { + if (std::find(nodes.begin(), nodes.end(), oc.second.node()) != + nodes.end()) { + write_node_map(writer, oc.second.node(), nodes); + } + } + + writer->write_end_element(); +} + +void ProjectSerializer230220::save(XmlStreamWriter *writer, + const SaveData &data, void *reserved) const +{ + if (!data.get_only_serialize_markers().empty()) { + writer->write_start_element("markers"); + + writer->write_attribute("version", std::to_string(1)); + + for (auto it = data.get_only_serialize_markers().cbegin(); + it != data.get_only_serialize_markers().cend(); it++) { + TimelineMarker *marker = *it; + writer->write_start_element("marker"); + marker->save(writer); + writer->write_end_element(); // marker + } + + writer->write_end_element(); // markers + } else if (!data.get_only_serialize_keyframes().empty()) { + writer->write_start_element("keyframes"); + + writer->write_attribute("version", std::to_string(1)); + + // Organize keyframes into node+input + std::map< + std::string, + std::map>>>> + organized; + + for (auto it = data.get_only_serialize_keyframes().cbegin(); + it != data.get_only_serialize_keyframes().cend(); it++) { + NodeKeyframe *key = *it; + organized[key->parent()->id()][key->input()][key->element()] + [key->track()] + .push_back(key); + } + + for (auto it = organized.cbegin(); it != organized.cend(); it++) { + writer->write_start_element("node"); + + writer->write_attribute("id", it->first); + + for (auto jt = it->second.cbegin(); jt != it->second.cend(); jt++) { + writer->write_start_element("input"); + + writer->write_attribute("id", jt->first); + + for (auto kt = jt->second.cbegin(); kt != jt->second.cend(); + kt++) { + writer->write_start_element("element"); + + writer->write_attribute("id", std::to_string(kt->first)); + + for (auto lt = kt->second.cbegin(); lt != kt->second.cend(); + lt++) { + const std::vector &keys = lt->second; + + writer->write_start_element("track"); + + writer->write_attribute("id", std::to_string(lt->first)); + + for (NodeKeyframe *key : keys) { + writer->write_start_element("key"); + key->save(writer, key->parent()->get_input_data_type( + key->input())); + writer->write_end_element(); // key + } + + writer->write_end_element(); // track + } + + writer->write_end_element(); // element + } + + writer->write_end_element(); // input + } + + writer->write_end_element(); // node; + } + + writer->write_end_element(); // keyframes + } else if (!data.get_only_serialize_nodes().empty()) { + if (data.type() == k_only_clips) { + writer->write_start_element("timeline"); + } else { + writer->write_start_element("nodes"); + } + + writer->write_attribute("version", std::to_string(1)); + + for (Node *n : data.get_only_serialize_nodes()) { + writer->write_start_element("node"); + + std::vector item_list; + for (Node *i : data.get_only_serialize_nodes()) { + if (i->is_item() && i->inputs_from(n, true)) { + item_list.push_back( + std::to_string(reinterpret_cast(i))); + } + } + if (!item_list.empty()) { + std::string joined; + for (size_t i = 0; i < item_list.size(); i++) { + if (i) { + joined += ','; + } + joined += item_list[i]; + } + writer->write_attribute("items", joined); + } + + n->save(writer); + writer->write_end_element(); // node + } + + if (!data.get_properties().empty()) { + writer->write_start_element("properties"); + for (auto it = data.get_properties().cbegin(); + it != data.get_properties().cend(); it++) { + writer->write_start_element("node"); + + writer->write_attribute( + "ptr", std::to_string(reinterpret_cast(it->first))); + + for (auto jt = it->second.cbegin(); jt != it->second.cend(); + jt++) { + writer->write_text_element(jt->first, jt->second); + } + + writer->write_end_element(); // node + } + writer->write_end_element(); // properties + } + + writer->write_end_element(); // nodes + } else if (Project *project = data.get_project()) { + writer->write_start_element("project"); + + writer->write_start_element("project"); + project->save(writer); + writer->write_end_element(); // project + + writer->write_start_element("layout"); + data.get_layout().to_xml(writer); + writer->write_end_element(); // layout + + writer->write_end_element(); // project + } else { + fprintf(stderr, "ProjectSerializer provided nothing to save\n"); + } +} + +void ProjectSerializer230220::post_connect(const std::vector &nodes, + SerializedData *project_data) const +{ + for (const SerializedData::SerializedConnection &con : + project_data->desired_connections) { + auto out_it = project_data->node_ptrs.find(con.output_node); + if (out_it != project_data->node_ptrs.end()) { + Node::connect_edge(out_it->second, con.input); + } + } + + for (const SerializedData::BlockLink &l : project_data->block_links) { + Node *a = l.block; + auto b_it = project_data->node_ptrs.find(l.link); + Node *b = b_it != project_data->node_ptrs.end() ? b_it->second : nullptr; + + Node::link(a, b); + } + + for (auto it = nodes.cbegin(); it != nodes.cend(); it++) { + Node *n = *it; + + n->PostLoadEvent(project_data); + + n->set_caches_enabled(true); + } +} + +} diff --git a/src/node/src/project/serializer/serializer230220.h b/src/node/src/project/serializer/serializer230220.h new file mode 100644 index 000000000..276dda04f --- /dev/null +++ b/src/node/src/project/serializer/serializer230220.h @@ -0,0 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PROJECTSERIALIZER230220_H +#define OAK_PROJECTSERIALIZER230220_H + +#include "serializer.h" + +namespace olive +{ + +class ProjectSerializer230220 : public ProjectSerializer { +public: + ProjectSerializer230220() = default; + +protected: + virtual LoadData load(Project *project, XmlStreamReader *reader, + LoadType load_type, void *reserved) const override; + + virtual void save(XmlStreamWriter *writer, const SaveData &data, + void *reserved) const override; + + virtual uint version() const override + { + return 230220; + } + +private: + void post_connect(const std::vector &nodes, + SerializedData *project_data) const; +}; + +} + +#endif // OAK_PROJECTSERIALIZER230220_H diff --git a/src/node/src/project/serializer/typeserializer.cpp b/src/node/src/project/serializer/typeserializer.cpp new file mode 100644 index 000000000..523c93d77 --- /dev/null +++ b/src/node/src/project/serializer/typeserializer.cpp @@ -0,0 +1,73 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "typeserializer.h" + +#include + +namespace olive +{ + +AudioParams TypeSerializer::load_audio_params(XmlStreamReader *reader) +{ + AudioParams a; + + while (xml_read_next_start_element(reader)) { + if (reader->name() == "samplerate") { + a.set_sample_rate(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "channellayout") { + a.set_channel_layout( + strtoull(reader->read_element_text().c_str(), nullptr, 10)); + } else if (reader->name() == "format") { + a.set_format( + SampleFormat::from_string(reader->read_element_text())); + } else if (reader->name() == "enabled") { + a.set_enabled(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "streamindex") { + a.set_stream_index(atoi(reader->read_element_text().c_str())); + } else if (reader->name() == "duration") { + a.set_duration( + strtoll(reader->read_element_text().c_str(), nullptr, 10)); + } else if (reader->name() == "timebase") { + a.set_time_base( + Rational::from_string(reader->read_element_text())); + } else { + reader->skip_current_element(); + } + } + + return a; +} + +void TypeSerializer::save_audio_params(XmlStreamWriter *writer, + const AudioParams &a) +{ + writer->write_text_element("samplerate", std::to_string(a.sample_rate())); + writer->write_text_element("channellayout", + std::to_string(a.channel_layout())); + writer->write_text_element("format", a.format().to_string()); + writer->write_text_element("enabled", std::to_string(a.enabled())); + writer->write_text_element("streamindex", std::to_string(a.stream_index())); + writer->write_text_element("duration", std::to_string(a.duration())); + writer->write_text_element("timebase", a.time_base().to_string()); +} + +} diff --git a/src/node/src/project/serializer/typeserializer.h b/src/node/src/project/serializer/typeserializer.h new file mode 100644 index 000000000..b82534319 --- /dev/null +++ b/src/node/src/project/serializer/typeserializer.h @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TYPESERIALIZER_H +#define OAK_TYPESERIALIZER_H + +#include + +#include "xmlutils.h" + +namespace olive +{ + +using namespace core; + +class TypeSerializer { +public: + TypeSerializer() = default; + + static AudioParams load_audio_params(XmlStreamReader *reader); + static void save_audio_params(XmlStreamWriter *writer, const AudioParams &a); +}; + +} + +#endif // OAK_TYPESERIALIZER_H diff --git a/src/node/src/serializeddata.cpp b/src/node/src/serializeddata.cpp new file mode 100644 index 000000000..20ded8c3b --- /dev/null +++ b/src/node/src/serializeddata.cpp @@ -0,0 +1,27 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "serializeddata.h" + +namespace olive +{ + +} diff --git a/src/node/src/serializeddata.h b/src/node/src/serializeddata.h new file mode 100644 index 000000000..e41872abc --- /dev/null +++ b/src/node/src/serializeddata.h @@ -0,0 +1,71 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SERIALIZEDDATA_H +#define OAK_SERIALIZEDDATA_H + +#include +#include +#include +#include + +#include "node.h" + +namespace olive +{ + +class NodeGroup; + +struct SerializedData { + struct SerializedConnection { + NodeInput input; + uintptr_t output_node; + }; + + struct BlockLink { + Node *block; + uintptr_t link; + }; + + struct GroupLink { + NodeGroup *group; + std::string passthrough_id; + uintptr_t input_node; + std::string input_id; + int input_element; + std::string custom_name; + InputFlags custom_flags; + NodeValue::Type data_type; + Variant default_val; + std::map custom_properties; + }; + + std::map> positions; + std::map node_ptrs; + std::vector desired_connections; + std::vector block_links; + std::vector group_input_links; + std::map group_output_links; +}; + +} + +#endif // OAK_SERIALIZEDDATA_H diff --git a/src/node/src/sliderdisplaytype.h b/src/node/src/sliderdisplaytype.h new file mode 100644 index 000000000..1cf0a9649 --- /dev/null +++ b/src/node/src/sliderdisplaytype.h @@ -0,0 +1,47 @@ +/*** + + Oak - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SLIDERDISPLAYTYPE_H +#define OAK_SLIDERDISPLAYTYPE_H + +namespace olive +{ + +/** + * @brief Slider display type enums shared by nodes and widgets + * + * Nodes reference these enums in their input properties ("view"); the + * slider widgets in app/widget/slider use the same values to render. The + * definitions live in the engine layer so nodes do not depend on widget + * headers. Enumerator order is ABI/feature compatible with the previous + * FloatSlider::DisplayType and RationalSlider::DisplayType. + */ +namespace slider +{ + +enum FloatDisplayType { k_normal, k_decibel, k_percentage }; + +enum RationalDisplayType { k_time, k_float, k_rational }; + +} // namespace slider + +} // namespace olive + +#endif // OAK_SLIDERDISPLAYTYPE_H diff --git a/src/node/src/splitvalue.h b/src/node/src/splitvalue.h new file mode 100644 index 000000000..59ab18983 --- /dev/null +++ b/src/node/src/splitvalue.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_SPLITVALUE_H +#define OAK_SPLITVALUE_H + +#include + +#include "variant.h" + +namespace olive +{ + +using SplitValue = std::vector; + +} + +#endif // OAK_SPLITVALUE_H diff --git a/src/node/src/time/CMakeLists.txt b/src/node/src/time/CMakeLists.txt new file mode 100644 index 000000000..7d7fe59ae --- /dev/null +++ b/src/node/src/time/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(timeformat) +add_subdirectory(timeoffset) +add_subdirectory(timeremap) + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + PARENT_SCOPE +) diff --git a/src/node/src/time/timeformat/CMakeLists.txt b/src/node/src/time/timeformat/CMakeLists.txt new file mode 100644 index 000000000..665bca754 --- /dev/null +++ b/src/node/src/time/timeformat/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/time/timeformat/timeformat.cpp + node/time/timeformat/timeformat.h + PARENT_SCOPE +) diff --git a/src/node/src/time/timeformat/timeformat.cpp b/src/node/src/time/timeformat/timeformat.cpp new file mode 100644 index 000000000..97c486ea9 --- /dev/null +++ b/src/node/src/time/timeformat/timeformat.cpp @@ -0,0 +1,195 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timeformat.h" + +#include +#include + +namespace olive +{ + +#define super Node + +const std::string TimeFormatNode::k_time_input = "time_in"; +const std::string TimeFormatNode::k_format_input = "format_in"; +const std::string TimeFormatNode::k_local_time_input = "localtime_in"; + +namespace { + +/** + * @brief Expand Qt date/time format tokens (QDateTime::toString syntax) + * + * Supports the field tokens d/dd, M/MM, yy/yyyy, h/hh, H/HH, m/mm, s/ss, + * z/zz/zzz, AP/ap/A/a, and single-quoted literal sections, mirroring Qt's + * longest-run matching. + */ +std::string format_date_time(const std::tm &tm, int ms, const std::string &format) +{ + // Qt displays h/hh on the 12-hour clock only when the format contains an + // AM/PM token (AP, ap, A or a); otherwise it is the 24-hour clock + bool has_am_pm = format.find("AP") != std::string::npos || + format.find("ap") != std::string::npos || + format.find('A') != std::string::npos || + format.find('a') != std::string::npos; + + std::string out; + for (size_t i = 0; i < format.size();) { + char c = format[i]; + if (c == '\'') { + size_t end = format.find('\'', i + 1); + if (end == std::string::npos) { + out.append(format, i + 1, std::string::npos); + break; + } + out.append(format, i + 1, end - (i + 1)); + i = end + 1; + continue; + } + size_t run = 1; + while (i + run < format.size() && format[i + run] == c) { + run++; + } + char buf[16]; + switch (c) { + case 'd': + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", tm.tm_mday); + out += buf; + break; + case 'M': + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", tm.tm_mon + 1); + out += buf; + break; + case 'y': + if (run >= 4) { + snprintf(buf, sizeof(buf), "%04d", tm.tm_year + 1900); + } else { + snprintf(buf, sizeof(buf), "%02d", (tm.tm_year + 1900) % 100); + } + out += buf; + break; + case 'H': + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", tm.tm_hour); + out += buf; + break; + case 'h': { + int hour = tm.tm_hour; + if (has_am_pm) { + hour %= 12; + if (hour == 0) { + hour = 12; + } + } + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", hour); + out += buf; + break; + } + case 'm': + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", tm.tm_min); + out += buf; + break; + case 's': + snprintf(buf, sizeof(buf), run >= 2 ? "%02d" : "%d", tm.tm_sec); + out += buf; + break; + case 'z': + snprintf(buf, sizeof(buf), run >= 3 ? "%03d" : "%d", ms); + out += buf; + break; + case 'A': + case 'a': { + // Qt: A/AP/ap/a are all replaced by the full AM/PM string; the + // two-letter form is a single token + if (i + run < format.size() && format[i + run] == char(c + ('P' - 'A'))) { + run++; + } + const char *am = (c == 'A') ? "AM" : "am"; + const char *pm = (c == 'A') ? "PM" : "pm"; + out += (tm.tm_hour < 12) ? am : pm; + break; + } + default: + out.append(format, i, run); + break; + } + i += run; + } + return out; +} + +} + +TimeFormatNode::TimeFormatNode() +{ + add_input(k_time_input, NodeValue::k_float); + add_input(k_format_input, NodeValue::k_text, "hh:mm:ss"); + add_input(k_local_time_input, NodeValue::k_boolean); +} + +std::string TimeFormatNode::name() const +{ + return "Time Format"; +} + +std::string TimeFormatNode::id() const +{ + return "org.olivevideoeditor.Olive.timeformat"; +} + +std::vector TimeFormatNode::category() const +{ + return { k_category_generator }; +} + +std::string TimeFormatNode::description() const +{ + return "Format time (in Unix epoch seconds) into a string."; +} + +void TimeFormatNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_time_input, "Time"); + set_input_name(k_format_input, "Format"); + set_input_name(k_local_time_input, "Interpret time as local time"); +} + +void TimeFormatNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + int64_t ms_since_epoch = value.at(k_time_input).to_double() * 1000; + bool time_is_local = value.at(k_local_time_input).to_bool(); + std::time_t secs = std::time_t(ms_since_epoch / 1000); + int ms = int(ms_since_epoch % 1000); + std::tm tm; + if (time_is_local) { + localtime_r(&secs, &tm); + } else { + gmtime_r(&secs, &tm); + } + std::string format = value.at(k_format_input).to_string(); + std::string output = format_date_time(tm, ms, format); + table->push(NodeValue(NodeValue::k_text, output, this)); +} + +} diff --git a/src/node/src/time/timeformat/timeformat.h b/src/node/src/time/timeformat/timeformat.h new file mode 100644 index 000000000..764949e13 --- /dev/null +++ b/src/node/src/time/timeformat/timeformat.h @@ -0,0 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TIMEFORMAT_H +#define OAK_TIMEFORMAT_H + +#include "node.h" + +namespace olive +{ + +class TimeFormatNode : public Node { +public: + TimeFormatNode(); + + NODE_DEFAULT_FUNCTIONS(TimeFormatNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_time_input; + static const std::string k_format_input; + static const std::string k_local_time_input; +}; + +} + +#endif // OAK_TIMEFORMAT_H diff --git a/src/node/src/time/timeoffset/CMakeLists.txt b/src/node/src/time/timeoffset/CMakeLists.txt new file mode 100644 index 000000000..602c2276a --- /dev/null +++ b/src/node/src/time/timeoffset/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/time/timeoffset/timeoffsetnode.cpp + node/time/timeoffset/timeoffsetnode.h + PARENT_SCOPE +) diff --git a/src/node/src/time/timeoffset/timeoffsetnode.cpp b/src/node/src/time/timeoffset/timeoffsetnode.cpp new file mode 100644 index 000000000..e740211f7 --- /dev/null +++ b/src/node/src/time/timeoffset/timeoffsetnode.cpp @@ -0,0 +1,96 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timeoffsetnode.h" + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string TimeOffsetNode::k_time_input = "time_in"; +const std::string TimeOffsetNode::k_input_input = "input_in"; + +#define super Node + +TimeOffsetNode::TimeOffsetNode() +{ + add_input(k_time_input, NodeValue::k_rational, Variant::from_value(Rational(0)), + InputFlags(k_input_flag_not_connectable)); + set_input_property(k_time_input, "view", slider::k_time); + set_input_property(k_time_input, "viewlock", true); + + add_input(k_input_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); +} + +void TimeOffsetNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_time_input, "Time"); + set_input_name(k_input_input, "Input"); +} + +TimeRange TimeOffsetNode::input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const +{ + if (input == k_input_input) { + return TimeRange(get_remapped_time(input_time.in()), + get_remapped_time(input_time.out())); + } else { + return super::input_time_adjustment(input, element, input_time, clamp); + } +} + +TimeRange +TimeOffsetNode::output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const +{ + if (input == k_input_input) { + // The inverse of InputTimeAdjustment(): times at the input are mapped + // back to the output by subtracting the offset again + return TimeRange(get_remapped_output_time(input_time.in()), + get_remapped_output_time(input_time.out())); + } else { + return super::output_time_adjustment(input, element, input_time); + } +} + +void TimeOffsetNode::value(const NodeValueRow &value, + const NodeGlobals &globals, + NodeValueTable *table) const +{ + table->push(value.at(k_input_input)); +} + +Rational TimeOffsetNode::get_remapped_time(const Rational &input) const +{ + return input + get_value_at_time(k_time_input, input).value(); +} + +Rational TimeOffsetNode::get_remapped_output_time(const Rational &input) const +{ + return input - get_value_at_time(k_time_input, input).value(); +} + +} diff --git a/src/node/src/time/timeoffset/timeoffsetnode.h b/src/node/src/time/timeoffset/timeoffsetnode.h new file mode 100644 index 000000000..bb2cd9b98 --- /dev/null +++ b/src/node/src/time/timeoffset/timeoffsetnode.h @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TIMEOFFSETNODE_H +#define OAK_TIMEOFFSETNODE_H + +#include "node.h" + +namespace olive +{ + +class TimeOffsetNode : public Node { +public: + TimeOffsetNode(); + + NODE_DEFAULT_FUNCTIONS(TimeOffsetNode) + + virtual std::string name() const override + { + return "Time Offset"; + } + + virtual std::string id() const override + { + return "org.olivevideoeditor.Olive.timeoffset"; + } + + virtual std::vector category() const override + { + return { k_category_time }; + } + + virtual std::string description() const override + { + return "Offset time passing through the graph."; + } + + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const override; + virtual TimeRange + output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_time_input; + static const std::string k_input_input; + +private: + Rational get_remapped_time(const Rational &input) const; + Rational get_remapped_output_time(const Rational &input) const; +}; + +} + +#endif // OAK_TIMEOFFSETNODE_H diff --git a/src/node/src/time/timeremap/CMakeLists.txt b/src/node/src/time/timeremap/CMakeLists.txt new file mode 100644 index 000000000..cfc6b5e80 --- /dev/null +++ b/src/node/src/time/timeremap/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/time/timeremap/timeremap.cpp + node/time/timeremap/timeremap.h + PARENT_SCOPE +) diff --git a/src/node/src/time/timeremap/timeremap.cpp b/src/node/src/time/timeremap/timeremap.cpp new file mode 100644 index 000000000..a79d3c2e3 --- /dev/null +++ b/src/node/src/time/timeremap/timeremap.cpp @@ -0,0 +1,109 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "timeremap.h" + +#include "sliderdisplaytype.h" + +namespace olive +{ + +const std::string TimeRemapNode::k_time_input = "time_in"; +const std::string TimeRemapNode::k_input_input = "input_in"; + +#define super Node + +TimeRemapNode::TimeRemapNode() +{ + add_input(k_time_input, NodeValue::k_rational, Variant::from_value(Rational(0)), + InputFlags(k_input_flag_not_connectable)); + set_input_property(k_time_input, "view", slider::k_time); + set_input_property(k_time_input, "viewlock", true); + + add_input(k_input_input, NodeValue::k_none, + InputFlags(k_input_flag_not_keyframable)); +} + +std::string TimeRemapNode::name() const +{ + return "Time Remap"; +} + +std::string TimeRemapNode::id() const +{ + return "org.olivevideoeditor.Olive.timeremap"; +} + +std::vector TimeRemapNode::category() const +{ + return { k_category_time }; +} + +std::string TimeRemapNode::description() const +{ + return "Arbitrarily remap time through the nodes."; +} + +TimeRange TimeRemapNode::input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const +{ + if (input == k_input_input) { + return TimeRange(get_remapped_time(input_time.in()), + get_remapped_time(input_time.out())); + } else { + return super::input_time_adjustment(input, element, input_time, clamp); + } +} + +TimeRange TimeRemapNode::output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const +{ + /*if (input == kInputInput) { + Rational target_time = GetValueAtTime(kTimeInput, input_time.in()).value(); + + return TimeRange(target_time, target_time + input_time.length()); + } else { + return super::output_time_adjustment(input, element, input_time); + }*/ + return super::output_time_adjustment(input, element, input_time); +} + +void TimeRemapNode::retranslate() +{ + super::retranslate(); + + set_input_name(k_time_input, "Time"); + set_input_name(k_input_input, "Input"); +} + +void TimeRemapNode::value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const +{ + table->push(value.at(k_input_input)); +} + +Rational TimeRemapNode::get_remapped_time(const Rational &input) const +{ + return get_value_at_time(k_time_input, input).value(); +} + +} diff --git a/src/node/src/time/timeremap/timeremap.h b/src/node/src/time/timeremap/timeremap.h new file mode 100644 index 000000000..2587b06df --- /dev/null +++ b/src/node/src/time/timeremap/timeremap.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_TIMEREMAPNODE_H +#define OAK_TIMEREMAPNODE_H + +#include "node.h" + +namespace olive +{ + +class TimeRemapNode : public Node { +public: + TimeRemapNode(); + + NODE_DEFAULT_FUNCTIONS(TimeRemapNode) + + virtual std::string name() const override; + virtual std::string id() const override; + virtual std::vector category() const override; + virtual std::string description() const override; + + virtual TimeRange input_time_adjustment(const std::string &input, int element, + const TimeRange &input_time, + bool clamp) const override; + virtual TimeRange + output_time_adjustment(const std::string &input, int element, + const TimeRange &input_time) const override; + + virtual void retranslate() override; + + virtual void value(const NodeValueRow &value, const NodeGlobals &globals, + NodeValueTable *table) const override; + + static const std::string k_time_input; + static const std::string k_input_input; + +private: + Rational get_remapped_time(const Rational &input) const; +}; + +} + +#endif // OAK_TIMEREMAPNODE_H diff --git a/src/node/src/traverser.cpp b/src/node/src/traverser.cpp new file mode 100644 index 000000000..b84965a84 --- /dev/null +++ b/src/node/src/traverser.cpp @@ -0,0 +1,563 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "traverser.h" + +#include +#include +#include + +#include "node.h" +#include "block/clip/clip.h" +#include "project/footage/footage.h" +#include "render/job/footagejob.h" +#include "render/rendermanager.h" +#include "render/job/pluginjob.h" + +namespace olive +{ + +NodeValueDatabase NodeTraverser::generate_database(const Node *node, + const TimeRange &range) +{ + NodeValueDatabase database; + + // HACK: Pick up loop mode from clips + LoopMode old_loop_mode = loop_mode_; + if (const ClipBlock *clip = dynamic_cast(node)) { + loop_mode_ = clip->loop_mode(); + } + + // We need to insert tables into the database for each input + auto ignore = node->ignore_inputs_for_rendering(); + for (const std::string &input : node->inputs()) { + if (is_cancelled()) { + return NodeValueDatabase(); + } + + if (std::find(ignore.begin(), ignore.end(), input) != ignore.end()) { + continue; + } + + database.insert(input, process_input(node, input, range)); + } + + loop_mode_ = old_loop_mode; + + return database; +} + +NodeValueRow NodeTraverser::generate_row(NodeValueDatabase *database, + const Node *node, + const TimeRange &range) +{ + // Generate row + NodeValueRow row; + for (auto it = database->begin(); it != database->end(); it++) { + // Get hint for which value should be pulled + NodeValue value = generate_row_value(node, it->first, &it->second, range); + row.insert({ it->first, value }); + } + + // TEMP: Audio needs to be refactored to work with new job system. But refactoring hasn't been + // done yet, so we emulate old behavior here JUST FOR AUDIO. + for (auto it = row.begin(); it != row.end(); it++) { + NodeValue &val = it->second; + if (val.type() == NodeValue::k_samples) { + resolve_jobs(val); + } + } + // END TEMP + + return row; +} + +NodeValueRow NodeTraverser::generate_row(const Node *node, + const TimeRange &range) +{ + // Generate database of input values of node + NodeValueDatabase database = generate_database(node, range); + + return generate_row(&database, node, range); +} + +NodeValue NodeTraverser::generate_row_value(const Node *node, + const std::string &input, + NodeValueTable *table, + const TimeRange &time) +{ + NodeValue value = generate_row_value_element(node, input, -1, table, time); + + if (value.array()) { + // Resolve each element of array + NodeValueTableArray tables = value.value(); + NodeValueArray output; + + for (auto it = tables.begin(); it != tables.end(); it++) { + output[it->first] = generate_row_value_element(node, input, it->first, + &it->second, time); + } + + value = NodeValue(value.type(), output, + value.source(), value.array(), value.tag()); + } + + return value; +} + +NodeValue NodeTraverser::generate_row_value_element(const Node *node, + const std::string &input, + int element, + NodeValueTable *table, + const TimeRange &time) +{ + int value_index = + generate_row_value_element_index(node->get_value_hint_for_input(input, element), + node->get_input_data_type(input), table); + + if (value_index == -1) { + // If value was -1, try getting the last value + value_index = table->count() - 1; + } + + if (value_index == -1) { + // If value is still -1, assume the table is empty and return nothing + return NodeValue(); + } + + NodeValue value = table->take_at(value_index); + + if (value.type() == NodeValue::k_texture && use_cache()) { + if (TexturePtr tex = value.to_texture()) { + std::lock_guard locker(node->video_frame_cache()->mutex()); + + node->video_frame_cache()->load_state(); + + std::string cache = + node->video_frame_cache()->get_valid_cache_filename(time.in()); + if (!cache.empty()) { + value.set_value(tex->to_job(CacheJob(cache, value))); + } + } + } + + return value; +} + +int NodeTraverser::generate_row_value_element_index(const Node::ValueHint &hint, + NodeValue::Type preferred_type, + const NodeValueTable *table) +{ + std::vector types = hint.types(); + + if (types.empty()) { + types.push_back(preferred_type); + } + + if (hint.index() == -1) { + // Get most recent value with this type and tag + return table->get_value_index(types, hint.tag()); + } else { + // Try to find value at this index + int index = table->count() - 1 - hint.index(); + int diff = 0; + + while (index + diff < table->count() && index - diff >= 0) { + if (index + diff < table->count() && + std::find(types.begin(), types.end(), + table->at(index + diff).type()) != types.end()) { + return index + diff; + } + if (index - diff >= 0 && + std::find(types.begin(), types.end(), + table->at(index - diff).type()) != types.end()) { + return index - diff; + } + diff++; + } + + return -1; + } +} + +int NodeTraverser::generate_row_value_element_index(const Node *node, + const std::string &input, + int element, + const NodeValueTable *table) +{ + return generate_row_value_element_index(node->get_value_hint_for_input(input, + element), + node->get_input_data_type(input), table); +} + +void NodeTraverser::transform(Matrix4x4 *transform, const Node *start, + const Node *end, const TimeRange &range) +{ + transform_ = transform; + transform_start_ = start; + transform_now_ = nullptr; + + generate_table(end, range); + + transform_ = nullptr; +} + +NodeValueTable NodeTraverser::process_input(const Node *node, + const std::string &input, + const TimeRange &range) +{ + // If input is connected, retrieve value directly + if (node->is_input_connected_for_render(input)) { + TimeRange adjusted_range = + node->input_time_adjustment(input, -1, range, true); + + // Value will equal something from the connected node, follow it + Node *output = node->get_connected_render_output(input); + NodeValueTable table = generate_table(output, adjusted_range, node); + return table; + + } else { + // Store node + Variant return_val; + bool is_array = node->input_is_array(input); + + if (is_array) { + // Value is an array, we will return a list of NodeValueTables + NodeValueTableArray array_tbl; + + Node::ActiveElements a = + node->get_active_elements_at_time(input, range); + if (a.mode() == Node::ActiveElements::k_all_elements) { + int sz = node->input_array_size(input); + for (int i = 0; i < sz; i++) { + process_input_element(array_tbl, node, input, i, range); + } + } else if (a.mode() == Node::ActiveElements::k_specified) { + for (int ele : a.elements()) { + process_input_element(array_tbl, node, input, ele, range); + } + } + + return_val = Variant::from_value(array_tbl); + + } else { + // Not connected or an array, just pull the immediate + TimeRange adjusted_range = + node->input_time_adjustment(input, -1, range, true); + + return_val = node->get_value_at_time(input, adjusted_range.in()); + } + + NodeValueTable return_table; + return_table.push(node->get_input_data_type(input), return_val, node, + is_array); + return return_table; + } +} + +void NodeTraverser::process_input_element(NodeValueTableArray &array_tbl, + const Node *node, const std::string &input, + int element, const TimeRange &range) +{ + NodeValueTable &sub_tbl = array_tbl[element]; + TimeRange adjusted_range = + node->input_time_adjustment(input, element, range, true); + + if (node->is_input_connected_for_render(input, element)) { + Node *output = node->get_connected_render_output(input, element); + sub_tbl = generate_table(output, adjusted_range, node); + } else { + Variant input_value = + node->get_value_at_time(input, adjusted_range.in(), element); + sub_tbl.push(node->get_input_data_type(input), input_value, node); + } +} + +NodeTraverser::NodeTraverser() + : cancel_(nullptr) + , transform_(nullptr) + , loop_mode_(LoopMode::k_loop_mode_off) +{ +} + +class GTTTime { +public: + GTTTime(const Node *n) + { + t = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + node = n; + } + + ~GTTTime() + { + fprintf(stderr, "GT for %p took %lld\n", (const void *)node, + (long long)(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count() - + t)); + } + + int64_t t; + const Node *node; +}; + +NodeValueTable NodeTraverser::generate_table(const Node *n, + const TimeRange &range, + const Node *next_node) +{ + // NOTE: Times how long a node takes to process, useful for profiling. + //GTTTime gtt(n);(void)gtt; + + // Use table cache to skip processing where available + auto cache_it = value_cache_.find(n); + if (cache_it != value_cache_.end()) { + auto &node_value_map = cache_it->second; + auto range_it = node_value_map.find(range); + if (range_it != node_value_map.end()) { + return range_it->second; + } + } + + // Generate row for node + NodeValueDatabase database = generate_database(n, range); + + // Check for bypass + bool is_enabled; + if (!database[Node::k_enabled_input].has(NodeValue::k_boolean)) { + // Fallback if we couldn't find a bool value + is_enabled = true; + } else { + is_enabled = + database[Node::k_enabled_input].get(NodeValue::k_boolean).to_bool(); + } + + NodeValueTable table; + + if (is_enabled) { + NodeValueRow row = generate_row(&database, n, range); + + // Generate output table + table = database.merge(); + + // By this point, the node should have all the inputs it needs to render correctly + NodeGlobals globals(video_params_, audio_params_, range, loop_mode_); + n->value(row, globals, &table); + + // `transform_now_` is the next node in the path that needs to be traversed. It only ever goes + // "down" the graph so that any traversing going back up doesn't unnecessarily transform + // from unrelated nodes or the same node twice + if (transform_) { + if (transform_now_ == n || transform_start_ == n) { + if (transform_now_ == n) { + Matrix4x4 t = n->gizmo_transformation(row, globals); + if (!t.is_identity()) { + (*transform_) *= t; + } + } + + transform_now_ = next_node; + } + } + } else { + // If this node has an effect input, ensure that is pushed last + NodeValueTable primary; + if (!n->get_effect_input_id().empty()) { + primary = database.take(n->get_effect_input_id()); + } + + table = database.merge(); + table.push(primary); + } + + value_cache_[n][range] = table; + + return table; +} + +TexturePtr NodeTraverser::process_video_cache_job(const CacheJob *val) +{ + return nullptr; +} + +TexturePtr NodeTraverser::process_plugin_job(TexturePtr texture, + TexturePtr destination, + const Node *node) +{ + // TODO + return nullptr; +} +Vector2D NodeTraverser::generate_resolution() const +{ + return Vector2D(video_params_.square_pixel_width(), + video_params_.height()); +} + +/** + * Resolve Jobs. I need to add a PluginJob here and move the plugin code here. + * @param val + */ +void NodeTraverser::resolve_jobs(NodeValue &val) +{ + if (val.type() == NodeValue::k_texture) { + if (TexturePtr job_tex = val.to_texture()) { + if (AcceleratedJob *base_job = job_tex->job()) { + auto resolved_it = resolved_texture_cache_.find(job_tex.get()); + if (resolved_it != resolved_texture_cache_.end()) { + val.set_value(resolved_it->second); + } else { + // Resolve any sub-jobs + for (auto it = base_job->get_values().begin(); + it != base_job->get_values().end(); it++) { + // Jobs will almost always be submitted with one of these types + NodeValue &subval = it->second; + resolve_jobs(subval); + } + + if (CacheJob *cj = dynamic_cast(base_job)) { + TexturePtr tex = process_video_cache_job(cj); + if (tex) { + val.set_value(tex); + } else { + val.set_value(cj->get_fallback()); + } + + } else if (ColorTransformJob *ctj = + dynamic_cast( + base_job)) { + VideoParams ctj_params = job_tex->params(); + + ctj_params.set_format(get_cache_video_params().format()); + + TexturePtr dest = create_texture(ctj_params); + + // Resolve input texture + NodeValue v = ctj->get_input_texture(); + resolve_jobs(v); + ctj->set_input_texture(v); + + process_color_transform(dest, val.source(), ctj); + + val.set_value(dest); + + } else if (ShaderJob *sj = + dynamic_cast(base_job)) { + VideoParams tex_params = job_tex->params(); + + TexturePtr tex = create_texture(tex_params); + + process_shader(tex, val.source(), sj); + + val.set_value(tex); + + } else if (GenerateJob *gj = + dynamic_cast(base_job)) { + VideoParams tex_params = job_tex->params(); + + TexturePtr tex = create_texture(tex_params); + + process_frame_generation(tex, val.source(), gj); + + // Convert to reference space + const std::string &colorspace = tex_params.colorspace(); + if (!colorspace.empty()) { + // Set format to primary format + tex_params.set_format( + get_cache_video_params().format()); + + TexturePtr dest = create_texture(tex_params); + + convert_to_reference_space(dest, tex, colorspace); + + tex = dest; + } + + val.set_value(tex); + + } else if (FootageJob *fj = + dynamic_cast(base_job)) { + Rational footage_time = Footage::adjust_time_by_loop_mode( + fj->time().in(), fj->loop_mode(), fj->length(), + fj->video_params().video_type(), + fj->video_params().frame_rate_as_time_base()); + + TexturePtr tex; + + if (footage_time.isNaN()) { + // Push dummy texture + tex = create_dummy_texture(fj->video_params()); + } else { + VideoParams managed_params = fj->video_params(); + managed_params.set_format( + get_cache_video_params().format()); + + tex = create_texture(managed_params); + if (tex) { + process_video_footage(tex, fj, footage_time); + } + } + + val.set_value(tex); + } else if (dynamic_cast(base_job)) { + VideoParams tex_params = job_tex->params(); + // Force internal working format (F32) for plugin processing, + // matching FootageJob/GenerateJob behavior. + tex_params.set_format(get_cache_video_params().format()); + tex_params.set_channel_count( + VideoParams::k_rgba_channel_count); + + TexturePtr tex = create_texture(tex_params); + + process_plugin_job(job_tex, tex, val.source()); + val.set_value(tex); + } + + // Cache resolved value + resolved_texture_cache_[job_tex.get()] = val.to_texture(); + } + } + } + + } else if (val.type() == NodeValue::k_samples) { + if (val.can_convert()) { + SampleJob job = val.value(); + SampleBuffer output_buffer = create_sample_buffer( + job.samples().audio_params(), job.samples().sample_count()); + process_samples(output_buffer, val.source(), job.time(), job); + val.set_value(output_buffer); + + } else if (val.can_convert()) { + FootageJob job = val.value(); + SampleBuffer buffer = + create_sample_buffer(get_cache_audio_params(), job.time().length()); + process_audio_footage(buffer, &job, job.time()); + val.set_value(buffer); + } + } +} + +TexturePtr NodeTraverser::create_dummy_texture(const VideoParams &p) +{ + return std::make_shared(p); +} + +} diff --git a/src/node/src/traverser.h b/src/node/src/traverser.h new file mode 100644 index 000000000..9b27d66d3 --- /dev/null +++ b/src/node/src/traverser.h @@ -0,0 +1,242 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODETRAVERSER_H +#define OAK_NODETRAVERSER_H + +#include +#include + +#include "codec/decoder.h" +#include "common/cancelableobject.h" +#include "output/track/track.h" +#include "render/job/cachejob.h" +#include "render/cancelatom.h" +#include "render/job/footagejob.h" +#include "render/job/colortransformjob.h" +#include "render/job/footagejob.h" +#include "valuedatabase.h" +#include "value.h" +#include "render/job/pluginjob.h" + +namespace olive +{ + +class NodeTraverser { +public: + NodeTraverser(); + + NodeValueTable generate_table(const Node *n, const TimeRange &range, + const Node *next_node = nullptr); + + virtual NodeValueDatabase generate_database(const Node *node, + const TimeRange &range); + + NodeValueRow generate_row(NodeValueDatabase *database, const Node *node, + const TimeRange &range); + NodeValueRow generate_row(const Node *node, const TimeRange &range); + + NodeValue generate_row_value(const Node *node, const std::string &input, + NodeValueTable *table, const TimeRange &time); + NodeValue generate_row_value_element(const Node *node, const std::string &input, + int element, NodeValueTable *table, + const TimeRange &time); + int generate_row_value_element_index(const Node::ValueHint &hint, + NodeValue::Type preferred_type, + const NodeValueTable *table); + int generate_row_value_element_index(const Node *node, const std::string &input, + int element, const NodeValueTable *table); + + void transform(Matrix4x4 *transform, const Node *start, const Node *end, + const TimeRange &range); + + const VideoParams &get_cache_video_params() const + { + return video_params_; + } + + void set_cache_video_params(const VideoParams ¶ms) + { + video_params_ = params; + } + + const AudioParams &get_cache_audio_params() const + { + return audio_params_; + } + + void set_cache_audio_params(const AudioParams ¶ms) + { + audio_params_ = params; + } + +protected: + NodeValueTable process_input(const Node *node, const std::string &input, + const TimeRange &range); + + void process_input_element(NodeValueTableArray &array_tbl, const Node *node, + const std::string &input, int element, + const TimeRange &range); + + virtual void process_video_footage(TexturePtr destination, + const FootageJob *stream, + const Rational &input_time) + { + } + + virtual void process_audio_footage(SampleBuffer &destination, + const FootageJob *stream, + const TimeRange &input_time) + { + } + + virtual void process_shader(TexturePtr destination, const Node *node, + const ShaderJob *job) + { + } + + virtual void process_color_transform(TexturePtr destination, const Node *node, + const ColorTransformJob *job) + { + } + + virtual void process_samples(SampleBuffer &destination, const Node *node, + const TimeRange &range, const SampleJob &job) + { + } + + virtual void process_frame_generation(TexturePtr destination, + const Node *node, + const GenerateJob *job) + { + } + + virtual void convert_to_reference_space(TexturePtr destination, + TexturePtr source, + const std::string &input_cs) + { + } + + virtual TexturePtr process_video_cache_job(const CacheJob *val); + + virtual TexturePtr create_texture(const VideoParams &p) + { + return create_dummy_texture(p); + } + + virtual SampleBuffer create_sample_buffer(const AudioParams ¶ms, + int sample_count) + { + // Return dummy by default + return SampleBuffer(); + } + + virtual TexturePtr process_plugin_job(TexturePtr texture, + TexturePtr destination, + const Node *node); + SampleBuffer create_sample_buffer(const AudioParams ¶ms, + const Rational &length) + { + if (params.is_valid()) { + return create_sample_buffer(params, params.time_to_samples(length)); + } else { + return SampleBuffer(); + } + } + + Vector2D generate_resolution() const; + + bool is_cancelled() + { + return cancel_ && cancel_->is_cancelled(); + } + + bool heard_cancel() const + { + return cancel_ && cancel_->heard_cancel(); + } + + CancelAtom *get_cancel_pointer() const + { + return cancel_; + } + void set_cancel_pointer(CancelAtom *cancel) + { + cancel_ = cancel; + } + + void resolve_jobs(NodeValue &value); + void resolve_audio_jobs(NodeValue &value); + + Block *get_current_block() const + { + return block_stack_.empty() ? nullptr : block_stack_.back(); + } + + LoopMode loop_mode() const + { + return loop_mode_; + } + + virtual bool use_cache() const + { + return false; + } + +private: + TexturePtr create_dummy_texture(const VideoParams &p); + + VideoParams video_params_; + + AudioParams audio_params_; + + CancelAtom *cancel_; + + const Node *transform_start_; + const Node *transform_now_; + Matrix4x4 *transform_; + + std::list block_stack_; + + LoopMode loop_mode_; + + /** + * @brief Ordering for TimeRange keys in the value cache (QHash + * replacement; std::map needs a strict weak ordering). + */ + struct TimeRangeLess { + bool operator()(const TimeRange &a, const TimeRange &b) const + { + if (a.in() != b.in()) { + return a.in() < b.in(); + } + return a.out() < b.out(); + } + }; + + std::map> + value_cache_; + std::map resolved_texture_cache_; +}; + +} + +#endif // OAK_NODETRAVERSER_H diff --git a/src/node/src/value.cpp b/src/node/src/value.cpp new file mode 100644 index 000000000..9284840cd --- /dev/null +++ b/src/node/src/value.cpp @@ -0,0 +1,495 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "value.h" + +#include +#include +#include + +#include "olive/core/util/stringutils.h" +#include "subtitleparams.h" +#include "videoparams.h" + +namespace olive +{ + +/** + * @brief Format a float like QString::number(f) ('g', 6 significant digits). + */ +static std::string number_to_string(double d) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", d); + return std::string(buf); +} + +std::string NodeValue::value_to_string(Type data_type, const Variant &value, + bool value_is_a_key_track) +{ + if (!value_is_a_key_track && data_type == k_vec2) { + Vector2D vec = value.value(); + + return number_to_string(vec.x()) + ":" + number_to_string(vec.y()); + } else if (!value_is_a_key_track && data_type == k_vec3) { + Vector3D vec = value.value(); + + return number_to_string(vec.x()) + ":" + number_to_string(vec.y()) + ":" + + number_to_string(vec.z()); + } else if (!value_is_a_key_track && data_type == k_vec4) { + Vector4D vec = value.value(); + + return number_to_string(vec.x()) + ":" + number_to_string(vec.y()) + ":" + + number_to_string(vec.z()) + ":" + number_to_string(vec.w()); + } else if (!value_is_a_key_track && data_type == k_color) { + Color c = value.value(); + + return number_to_string(c.red()) + ":" + number_to_string(c.green()) + ":" + + number_to_string(c.blue()) + ":" + number_to_string(c.alpha()); + } else if (!value_is_a_key_track && data_type == k_bezier) { + Bezier b = value.value(); + + return number_to_string(b.x()) + ":" + number_to_string(b.y()) + ":" + + number_to_string(b.cp1_x()) + ":" + number_to_string(b.cp1_y()) + ":" + + number_to_string(b.cp2_x()) + ":" + number_to_string(b.cp2_y()); + } else if (data_type == k_rational) { + return value.value().to_string(); + } else if (data_type == k_texture || data_type == k_samples || + data_type == k_none) { + // These data types need no XML representation + return std::string(); + } else if (data_type == k_int) { + return std::to_string(value.value()); + } else if (data_type == k_binary) { + return byte_array_to_base64(value.to_byte_array()); + } else { + if (value.can_convert()) { + return value.to_string(); + } + + if (!value.is_null()) { + fprintf(stderr, "Failed to convert type %x to string\n", + (unsigned int)(data_type)); + } + + return std::string(); + } +} + +std::vector +NodeValue::split_normal_value_into_track_values(Type type, const Variant &value) +{ + std::vector vals(get_number_of_keyframe_tracks(type)); + + switch (type) { + case k_vec2: { + Vector2D vec = value.value(); + vals[0] = vec.x(); + vals[1] = vec.y(); + break; + } + case k_vec3: { + Vector3D vec = value.value(); + vals[0] = vec.x(); + vals[1] = vec.y(); + vals[2] = vec.z(); + break; + } + case k_vec4: { + Vector4D vec = value.value(); + vals[0] = vec.x(); + vals[1] = vec.y(); + vals[2] = vec.z(); + vals[3] = vec.w(); + break; + } + case k_color: { + Color c = value.value(); + vals[0] = c.red(); + vals[1] = c.green(); + vals[2] = c.blue(); + vals[3] = c.alpha(); + break; + } + case k_bezier: { + Bezier b = value.value(); + vals[0] = b.x(); + vals[1] = b.y(); + vals[2] = b.cp1_x(); + vals[3] = b.cp1_y(); + vals[4] = b.cp2_x(); + vals[5] = b.cp2_y(); + break; + } + default: + vals[0] = value; + } + + return vals; +} + +Variant NodeValue::combine_track_values_into_normal_value( + Type type, const std::vector &split) +{ + if (split.empty()) { + return Variant(); + } + + switch (type) { + case k_vec2: { + return Vector2D(split.at(0).to_float(), split.at(1).to_float()); + } + case k_vec3: { + return Vector3D(split.at(0).to_float(), split.at(1).to_float(), + split.at(2).to_float()); + } + case k_vec4: { + return Vector4D(split.at(0).to_float(), split.at(1).to_float(), + split.at(2).to_float(), split.at(3).to_float()); + } + case k_color: { + return Variant::from_value( + Color(split.at(0).to_float(), split.at(1).to_float(), + split.at(2).to_float(), split.at(3).to_float())); + } + case k_bezier: + return Variant::from_value( + Bezier(split.at(0).to_double(), split.at(1).to_double(), + split.at(2).to_double(), split.at(3).to_double(), + split.at(4).to_double(), split.at(5).to_double())); + default: + return split.front(); + } +} + +int NodeValue::get_number_of_keyframe_tracks(Type type) +{ + switch (type) { + case NodeValue::k_vec2: + return 2; + case NodeValue::k_vec3: + return 3; + case NodeValue::k_vec4: + case NodeValue::k_color: + return 4; + case NodeValue::k_bezier: + return 6; + default: + return 1; + } +} + +Variant NodeValue::string_to_value(Type data_type, const std::string &string, + bool value_is_a_key_track) +{ + if (!value_is_a_key_track && data_type == k_vec2) { + std::vector vals = core::StringUtils::split(string, ':'); + + validate_vector_string(&vals, 2); + + return Vector2D(strtof(vals.at(0).c_str(), nullptr), + strtof(vals.at(1).c_str(), nullptr)); + } else if (!value_is_a_key_track && data_type == k_vec3) { + std::vector vals = core::StringUtils::split(string, ':'); + + validate_vector_string(&vals, 3); + + return Vector3D(strtof(vals.at(0).c_str(), nullptr), + strtof(vals.at(1).c_str(), nullptr), + strtof(vals.at(2).c_str(), nullptr)); + } else if (!value_is_a_key_track && data_type == k_vec4) { + std::vector vals = core::StringUtils::split(string, ':'); + + validate_vector_string(&vals, 4); + + return Vector4D(strtof(vals.at(0).c_str(), nullptr), + strtof(vals.at(1).c_str(), nullptr), + strtof(vals.at(2).c_str(), nullptr), + strtof(vals.at(3).c_str(), nullptr)); + } else if (!value_is_a_key_track && data_type == k_color) { + std::vector vals = core::StringUtils::split(string, ':'); + + validate_vector_string(&vals, 4); + + return Variant::from_value( + Color(strtod(vals.at(0).c_str(), nullptr), + strtod(vals.at(1).c_str(), nullptr), + strtod(vals.at(2).c_str(), nullptr), + strtod(vals.at(3).c_str(), nullptr))); + } else if (!value_is_a_key_track && data_type == k_bezier) { + std::vector vals = core::StringUtils::split(string, ':'); + + validate_vector_string(&vals, 6); + + return Variant::from_value( + Bezier(strtod(vals.at(0).c_str(), nullptr), + strtod(vals.at(1).c_str(), nullptr), + strtod(vals.at(2).c_str(), nullptr), + strtod(vals.at(3).c_str(), nullptr), + strtod(vals.at(4).c_str(), nullptr), + strtod(vals.at(5).c_str(), nullptr))); + } else if (data_type == k_int) { + return Variant::from_value(strtoll(string.c_str(), nullptr, 10)); + } else if (data_type == k_rational) { + return Variant::from_value(Rational::from_string(string)); + } else if (data_type == k_binary) { + return byte_array_from_base64(string); + } else { + return string; + } +} + +void NodeValue::validate_vector_string(std::vector *list, int count) +{ + while (list->size() < size_t(count)) { + list->push_back("0"); + } +} + +std::string NodeValue::get_pretty_data_type_name(Type type) +{ + switch (type) { + case k_none: + return "None"; + case k_int: + case k_combo: + return "Integer"; + case k_str_combo: + return "String Combo"; + case k_float: + return "Float"; + case k_rational: + return "Rational"; + case k_boolean: + return "Boolean"; + case k_color: + return "Color"; + case k_matrix: + return "Matrix"; + case k_text: + return "Text"; + case k_font: + return "Font"; + case k_file: + return "File"; + case k_texture: + return "Texture"; + case k_samples: + return "Samples"; + case k_vec2: + return "Vector 2D"; + case k_vec3: + return "Vector 3D"; + case k_vec4: + return "Vector 4D"; + case k_bezier: + return "Bezier"; + case k_video_params: + return "Video Parameters"; + case k_audio_params: + return "Audio Parameters"; + case k_subtitle_params: + return "Subtitle Parameters"; + case k_binary: + return "Binary"; + case k_push_button: + return "Push Button"; + + case k_data_type_count: + break; + } + + return "Unknown"; +} + +std::string NodeValue::get_data_type_name(Type type) +{ + switch (type) { + case k_none: + return "none"; + case k_int: + return "int"; + case k_combo: + return "combo"; + case k_str_combo: + return "strcombo"; + case k_float: + return "float"; + case k_rational: + return "Rational"; + case k_boolean: + return "bool"; + case k_color: + return "color"; + case k_matrix: + return "matrix"; + case k_text: + return "text"; + case k_font: + return "font"; + case k_file: + return "file"; + case k_texture: + return "texture"; + case k_samples: + return "samples"; + case k_vec2: + return "vec2"; + case k_vec3: + return "vec3"; + case k_vec4: + return "vec4"; + case k_bezier: + return "bezier"; + case k_video_params: + return "vparam"; + case k_audio_params: + return "aparam"; + case k_subtitle_params: + return "sparam"; + case k_binary: + return "binary"; + case k_push_button: + return "pushbutton"; + case k_data_type_count: + break; + } + + return std::string(); +} + +NodeValue::Type NodeValue::get_data_type_from_name(const std::string &n) +{ + // Slow but easy to maintain + for (int i = 0; i < k_data_type_count; i++) { + Type t = static_cast(i); + if (get_data_type_name(t) == n) { + return t; + } + } + + return NodeValue::k_none; +} + +NodeValue NodeValueTable::get(const std::vector &type, + const std::string &tag) const +{ + int value_index = get_value_index(type, tag); + + if (value_index >= 0) { + return values_.at(value_index); + } + + return NodeValue(); +} + +NodeValue NodeValueTable::take(const std::vector &type, + const std::string &tag) +{ + int value_index = get_value_index(type, tag); + + if (value_index >= 0) { + return take_at(value_index); + } + + return NodeValue(); +} + +bool NodeValueTable::has(NodeValue::Type type) const +{ + for (int i = int(values_.size()) - 1; i >= 0; i--) { + const NodeValue &v = values_.at(i); + + if (v.type() == type) { + return true; + } + } + + return false; +} + +void NodeValueTable::remove(const NodeValue &v) +{ + for (int i = int(values_.size()) - 1; i >= 0; i--) { + const NodeValue &compare = values_.at(i); + + if (compare == v) { + values_.erase(values_.begin() + i); + return; + } + } +} + +NodeValueTable NodeValueTable::merge(std::vector tables) +{ + if (tables.size() == 1) { + return tables.front(); + } + + int row = 0; + + NodeValueTable merged_table; + + // Slipstreams all tables together + while (true) { + bool all_merged = true; + + for (const NodeValueTable &t : tables) { + if (row < t.count()) { + all_merged = false; + } else { + continue; + } + + int row_index = t.count() - 1 - row; + + merged_table.prepend(t.at(row_index)); + } + + row++; + + if (all_merged) { + break; + } + } + + return merged_table; +} + +int NodeValueTable::get_value_index(const std::vector &types, + const std::string &tag) const +{ + int index = -1; + + for (int i = int(values_.size()) - 1; i >= 0; i--) { + const NodeValue &v = values_.at(i); + + if (std::find(types.begin(), types.end(), v.type()) != types.end() && + (tag.empty() || tag == v.tag())) { + index = i; + break; + } + } + + return index; +} + +} diff --git a/src/node/src/value.h b/src/node/src/value.h new file mode 100644 index 000000000..68ba39f2e --- /dev/null +++ b/src/node/src/value.h @@ -0,0 +1,544 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEVALUE_H +#define OAK_NODEVALUE_H + +#include +#include +#include + +#include "mathtypes.h" +#include "splitvalue.h" +#include "variant.h" +#include "olive/core/util/bezier.h" +#include "olive/core/util/color.h" +#include "olive/core/util/rational.h" +#include "render/texture.h" + +namespace olive +{ + +using core::Bezier; +using core::Color; +using core::Rational; + +class Node; +class NodeValue; +class NodeValueTable; + +using NodeValueArray = std::map; +using NodeValueTableArray = std::map; + +class NodeValue { +public: + /** + * @brief The types of data that can be passed between Nodes + */ + enum Type { + k_none, + + /** + ****************************** SPECIFIC IDENTIFIERS ****************************** + */ + + /** + * Integer type + * + * Resolves to int64_t. + */ + k_int, + + /** + * Decimal (floating-point) type + * + * Resolves to `double`. + */ + k_float, + + /** + * Decimal (Rational) type + * + * Resolves to `double`. + */ + k_rational, + + /** + * Boolean type + * + * Resolves to `bool`. + */ + k_boolean, + + /** + * Floating-point type + * + * Resolves to `Color`. + * + * Colors passed around the nodes should always be in reference space and preferably use + */ + k_color, + + /** + * Matrix type + * + * Resolves to `Matrix4x4`. + */ + k_matrix, + + /** + * Text type + * + * Resolves to `std::string`. + */ + k_text, + + /** + * Font type + * + * Resolves to `std::string` (the font family name). + */ + k_font, + + /** + * File type + * + * Resolves to a `std::string` containing an absolute file path. + */ + k_file, + + /** + * Image buffer type + * + * True value type depends on the render engine used. + */ + k_texture, + + /** + * Audio samples type + * + * Resolves to `SampleBuffer`. + */ + k_samples, + + /** + * Two-dimensional vector (XY) type + * + * Resolves to `Vector2D`. + */ + k_vec2, + + /** + * Three-dimensional vector (XYZ) type + * + * Resolves to `Vector3D`. + */ + k_vec3, + + /** + * Four-dimensional vector (XYZW) type + * + * Resolves to `Vector4D`. + */ + k_vec4, + + /** + * Cubic bezier type that contains three X/Y coordinates, the main point, and two control points + * + * Resolves to `Bezier` + */ + k_bezier, + + /** + * ComboBox type + * + * Resolves to `int` - the index currently selected + */ + k_combo, + /** + * ComboBox type + * + * Resolves to `std::string` - the text of choice currently selected + * This is to support the OpenFX type kOfxParamTypeStrChoice + */ + k_str_combo, + /** + * Video Parameters type + * + * Resolves to `VideoParams` + */ + k_video_params, + + /** + * Audio Parameters type + * + * Resolves to `AudioParams` + */ + k_audio_params, + + /** + * Subtitle Parameters type + * + * Resolves to `SubtitleParams` + */ + k_subtitle_params, + + /** + * Binary Data + */ + k_binary, + + /** + *Push Button + */ + k_push_button, + /** + * End of list + */ + k_data_type_count + }; + + NodeValue() + : type_(k_none) + , from_(nullptr) + , array_(false) + { + } + + template + NodeValue(Type type, const T &data, const Node *from = nullptr, + bool array = false, const std::string &tag = std::string()) + : type_(type) + , from_(from) + , tag_(tag) + , array_(array) + { + set_value(data); + } + + template + NodeValue(Type type, const T &data, const Node *from, const std::string &tag) + : NodeValue(type, data, from, false, tag) + { + } + + Type type() const + { + return type_; + } + + template T value() const + { + return data_.value(); + } + + template void set_value(const T &v) + { + data_ = Variant::from_value(v); + } + + const Variant &data() const + { + return data_; + } + + template bool can_convert() const + { + return data_.can_convert(); + } + + const std::string &tag() const + { + return tag_; + } + + void set_tag(const std::string &tag) + { + tag_ = tag; + } + + const Node *source() const + { + return from_; + } + + bool array() const + { + return array_; + } + + bool operator==(const NodeValue &rhs) const + { + return type_ == rhs.type_ && tag_ == rhs.tag_ && data_ == rhs.data_; + } + + operator bool() const + { + return !data_.is_null(); + } + + static std::string get_pretty_data_type_name(Type type); + + static std::string get_data_type_name(Type type); + static NodeValue::Type get_data_type_from_name(const std::string &n); + + static std::string value_to_string(Type data_type, const Variant &value, + bool value_is_a_key_track); + static std::string value_to_string(const NodeValue &v, bool value_is_a_key_track) + { + return value_to_string(v.type_, v.data_, value_is_a_key_track); + } + + static Variant string_to_value(Type data_type, const std::string &string, + bool value_is_a_key_track); + + static std::vector + split_normal_value_into_track_values(Type type, const Variant &value); + + static Variant + combine_track_values_into_normal_value(Type type, + const std::vector &split); + + SplitValue to_split_value() const + { + return split_normal_value_into_track_values(type_, data_); + } + + /** + * @brief Returns whether a data type can be interpolated or not + */ + static bool type_can_be_interpolated(NodeValue::Type type) + { + return type == k_float || type == k_vec2 || type == k_vec3 || + type == k_vec4 || type == k_bezier || type == k_color || + type == k_rational; + } + + static bool type_is_numeric(NodeValue::Type type) + { + return type == k_float || type == k_int || type == k_rational; + } + + static bool type_is_vector(NodeValue::Type type) + { + return type == k_vec2 || type == k_vec3 || type == k_vec4; + } + + static bool type_is_buffer(NodeValue::Type type) + { + return type == k_texture || type == k_samples; + } + + static int get_number_of_keyframe_tracks(Type type); + + static void validate_vector_string(std::vector *list, int count); + + TexturePtr to_texture() const + { + return value(); + } + SampleBuffer to_samples() const + { + return value(); + } + bool to_bool() const + { + return value(); + } + double to_double() const + { + return value(); + } + int64_t to_int() const + { + return value(); + } + Rational to_rational() const + { + return value(); + } + std::string to_string() const + { + return value(); + } + Color to_color() const + { + return value(); + } + Matrix4x4 to_matrix() const + { + return value(); + } + VideoParams to_video_params() const + { + return value(); + } + AudioParams to_audio_params() const + { + return value(); + } + Vector2D to_vec2() const + { + return value(); + } + Vector3D to_vec3() const + { + return value(); + } + Vector4D to_vec4() const + { + return value(); + } + Bezier to_bezier() const + { + return value(); + } + NodeValueArray to_array() const + { + return value(); + } + +private: + Type type_; + Variant data_; + const Node *from_; + std::string tag_; + bool array_; +}; + +class NodeValueTable { +public: + NodeValueTable() = default; + + NodeValue get(NodeValue::Type type, const std::string &tag = std::string()) const + { + std::vector types = { type }; + return get(types, tag); + } + + NodeValue get(const std::vector &type, + const std::string &tag = std::string()) const; + + NodeValue take(NodeValue::Type type, const std::string &tag = std::string()) + { + std::vector types = { type }; + return take(types, tag); + } + + NodeValue take(const std::vector &type, + const std::string &tag = std::string()); + + void push(const NodeValue &value) + { + values_.push_back(value); + } + + void push(const NodeValueTable &value) + { + values_.insert(values_.end(), value.values_.begin(), value.values_.end()); + } + + template + void push(NodeValue::Type type, const T &data, const Node *from, + bool array = false, const std::string &tag = std::string()) + { + push(NodeValue(type, data, from, array, tag)); + } + + template + void push(NodeValue::Type type, const T &data, const Node *from, + const std::string &tag) + { + push(NodeValue(type, data, from, false, tag)); + } + + void prepend(const NodeValue &value) + { + values_.insert(values_.begin(), value); + } + + template + void prepend(NodeValue::Type type, const T &data, const Node *from, + bool array = false, const std::string &tag = std::string()) + { + prepend(NodeValue(type, data, from, array, tag)); + } + + template + void prepend(NodeValue::Type type, const T &data, const Node *from, + const std::string &tag) + { + prepend(NodeValue(type, data, from, false, tag)); + } + + const NodeValue &at(int index) const + { + return values_.at(index); + } + NodeValue take_at(int index) + { + NodeValue v = values_.at(index); + values_.erase(values_.begin() + index); + return v; + } + + int count() const + { + return values_.size(); + } + + bool has(NodeValue::Type type) const; + void remove(const NodeValue &v); + + void clear() + { + values_.clear(); + } + + bool is_empty() const + { + return values_.empty(); + } + + bool operator==(const NodeValueTable &rhs) const + { + return values_ == rhs.values_; + } + + bool operator!=(const NodeValueTable &rhs) const + { + return !(*this == rhs); + } + + int get_value_index(const std::vector &type, + const std::string &tag) const; + + static NodeValueTable merge(std::vector tables); + +private: + std::vector values_; +}; + +using NodeValueRow = std::map; + +} + +#endif // OAK_NODEVALUE_H diff --git a/src/node/src/valuedatabase.cpp b/src/node/src/valuedatabase.cpp new file mode 100644 index 000000000..26d1951f3 --- /dev/null +++ b/src/node/src/valuedatabase.cpp @@ -0,0 +1,43 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "valuedatabase.h" + +namespace olive +{ + +NodeValueTable NodeValueDatabase::merge() const +{ + std::map copy = tables_; + + // Kinda hacky, but we don't need this table to slipstream + copy.erase("global"); + + std::vector values; + values.reserve(copy.size()); + for (const auto &p : copy) { + values.push_back(p.second); + } + + return NodeValueTable::merge(values); +} + +} diff --git a/src/node/src/valuedatabase.h b/src/node/src/valuedatabase.h new file mode 100644 index 000000000..1f748cc86 --- /dev/null +++ b/src/node/src/valuedatabase.h @@ -0,0 +1,93 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEVALUEDATABASE_H +#define OAK_NODEVALUEDATABASE_H + +#include "param.h" +#include "value.h" + +namespace olive +{ + +class NodeValueDatabase { +public: + NodeValueDatabase() = default; + + NodeValueTable &operator[](const std::string &input_id) + { + return tables_[input_id]; + } + + void insert(const std::string &key, const NodeValueTable &value) + { + tables_[key] = value; + } + + NodeValueTable take(const std::string &key) + { + auto it = tables_.find(key); + if (it == tables_.end()) { + return NodeValueTable(); + } + NodeValueTable value = std::move(it->second); + tables_.erase(it); + return value; + } + + NodeValueTable merge() const; + + using Tables = std::map; + using const_iterator = Tables::const_iterator; + using iterator = Tables::iterator; + + inline const_iterator cbegin() const + { + return tables_.cbegin(); + } + + inline const_iterator cend() const + { + return tables_.cend(); + } + + inline iterator begin() + { + return tables_.begin(); + } + + inline iterator end() + { + return tables_.end(); + } + + inline bool contains(const std::string &s) const + { + return tables_.count(s); + } + +private: + Tables tables_; +}; + +} + +#endif // OAK_NODEVALUEDATABASE_H diff --git a/src/node/src/variant.cpp b/src/node/src/variant.cpp new file mode 100644 index 000000000..28bd95bd0 --- /dev/null +++ b/src/node/src/variant.cpp @@ -0,0 +1,314 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "variant.h" + +#include +#include +#include + +namespace olive +{ + +/** + * @brief Format a double like QString::number(d) ('g', 6 significant digits). + */ +static std::string format_double(double d) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", d); + return std::string(buf); +} + +double Variant::to_double(bool *ok) const +{ + if (is_numeric_kind()) { + if (ok) { + *ok = true; + } + return numeric_as_double(); + } + + if (kind_ == k_string) { + char *end = nullptr; + double d = std::strtod(string_.c_str(), &end); + bool valid = end != string_.c_str(); + if (ok) { + *ok = valid; + } + return valid ? d : 0.0; + } + + if (ok) { + *ok = false; + } + return 0.0; +} + +float Variant::to_float(bool *ok) const +{ + return float(to_double(ok)); +} + +int64_t Variant::to_long_long(bool *ok) const +{ + if (is_numeric_kind()) { + if (ok) { + *ok = true; + } + return numeric_as_int64(); + } + + if (kind_ == k_string) { + char *end = nullptr; + long long i = std::strtoll(string_.c_str(), &end, 10); + bool valid = end != string_.c_str(); + if (ok) { + *ok = valid; + } + return valid ? int64_t(i) : 0; + } + + if (ok) { + *ok = false; + } + return 0; +} + +uint64_t Variant::to_u_long_long(bool *ok) const +{ + if (is_numeric_kind()) { + if (ok) { + *ok = true; + } + return numeric_as_uint64(); + } + + if (kind_ == k_string) { + char *end = nullptr; + unsigned long long i = std::strtoull(string_.c_str(), &end, 10); + bool valid = end != string_.c_str(); + if (ok) { + *ok = valid; + } + return valid ? uint64_t(i) : 0; + } + + if (ok) { + *ok = false; + } + return 0; +} + +int Variant::to_int(bool *ok) const +{ + return int(to_long_long(ok)); +} + +unsigned int Variant::to_uint(bool *ok) const +{ + return (unsigned int)(to_u_long_long(ok)); +} + +bool Variant::to_bool() const +{ + if (is_numeric_kind()) { + return numeric_as_double() != 0.0; + } + + if (kind_ == k_string) { + if (string_ == "true" || string_ == "TRUE") { + return true; + } + if (string_ == "false" || string_ == "FALSE" || string_.empty()) { + return false; + } + // QVariant falls back to numeric interpretation + bool ok = false; + double d = to_double(&ok); + return ok && d != 0.0; + } + + return false; +} + +std::string Variant::to_string() const +{ + switch (kind_) { + case k_null: + return std::string(); + case k_bool: + return bool_ ? std::string("true") : std::string("false"); + case k_int: + return std::to_string(int_); + case k_uint: + return std::to_string(uint_); + case k_double: + return format_double(double_); + case k_string: + return string_; + case k_byte_array: + return std::string(byte_array_.begin(), byte_array_.end()); + case k_string_list: + case k_custom: + return std::string(); + } + + return std::string(); +} + +StringList Variant::to_string_list() const +{ + if (kind_ == k_string_list) { + return string_list_; + } + + if (kind_ == k_string) { + // QString -> QStringList conversion wraps the string in a list + return StringList{ string_ }; + } + + return StringList(); +} + +ByteArray Variant::to_byte_array() const +{ + if (kind_ == k_byte_array) { + return byte_array_; + } + + if (kind_ == k_string) { + return ByteArray(string_.begin(), string_.end()); + } + + return ByteArray(); +} + +bool Variant::operator==(const Variant &rhs) const +{ + if (kind_ == k_null || rhs.kind_ == k_null) { + return kind_ == rhs.kind_; + } + + if (is_numeric_kind() && rhs.is_numeric_kind()) { + // QVariant compares numeric types numerically across kinds + if (kind_ == k_double || rhs.kind_ == k_double) { + return numeric_as_double() == rhs.numeric_as_double(); + } + if (kind_ == k_uint || rhs.kind_ == k_uint) { + return numeric_as_uint64() == rhs.numeric_as_uint64(); + } + return numeric_as_int64() == rhs.numeric_as_int64(); + } + + if (kind_ != rhs.kind_) { + return false; + } + + switch (kind_) { + case k_string: + return string_ == rhs.string_; + case k_string_list: + return string_list_ == rhs.string_list_; + case k_byte_array: + return byte_array_ == rhs.byte_array_; + case k_custom: + return custom_->equals(rhs.custom_.get()); + default: + return false; + } +} + +/** + * @brief QByteArray::toBase64() equivalent. + */ +std::string byte_array_to_base64(const ByteArray &data) +{ + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string out; + out.reserve((data.size() + 2) / 3 * 4); + + for (size_t i = 0; i < data.size(); i += 3) { + uint32_t chunk = uint8_t(data[i]) << 16; + if (i + 1 < data.size()) { + chunk |= uint8_t(data[i + 1]) << 8; + } + if (i + 2 < data.size()) { + chunk |= uint8_t(data[i + 2]); + } + + out += table[(chunk >> 18) & 0x3F]; + out += table[(chunk >> 12) & 0x3F]; + out += (i + 1 < data.size()) ? table[(chunk >> 6) & 0x3F] : '='; + out += (i + 2 < data.size()) ? table[chunk & 0x3F] : '='; + } + + return out; +} + +/** + * @brief QByteArray::fromBase64() equivalent. + */ +ByteArray byte_array_from_base64(const std::string &text) +{ + auto decode_char = [](char c) -> int { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '+') { + return 62; + } + if (c == '/') { + return 63; + } + return -1; + }; + + ByteArray out; + int accumulator = 0; + int bits = 0; + + for (char c : text) { + if (c == '=' || std::isspace((unsigned char)(c))) { + continue; + } + int v = decode_char(c); + if (v < 0) { + continue; + } + accumulator = (accumulator << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back(char((accumulator >> bits) & 0xFF)); + } + } + + return out; +} + +} diff --git a/src/node/src/variant.h b/src/node/src/variant.h new file mode 100644 index 000000000..b08f35187 --- /dev/null +++ b/src/node/src/variant.h @@ -0,0 +1,546 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODEVARIANT_H +#define OAK_NODEVARIANT_H + +#include +#include +#include +#include +#include +#include +#include + +namespace olive +{ + +/** + * @brief De-Qt replacement for QStringList. + */ +using StringList = std::vector; + +/** + * @brief De-Qt replacement for QByteArray. + */ +using ByteArray = std::vector; + +/** + * @brief De-Qt replacement for QVariant. + * + * A discriminated value container covering the data set actually carried + * through the node module: + * + * - null (default-constructed) + * - bool + * - signed integer (all signed integer types are stored as int64_t) + * - unsigned integer (stored as uint64_t) + * - floating point (float and double are both stored as double) + * - string (std::string, replaces QString) + * - string list (StringList, replaces QStringList) + * - byte array (ByteArray, replaces QByteArray) + * - any other copy-constructible type, held type-erased (replaces + * Q_DECLARE_METATYPE custom types such as Color, Vector2D, TexturePtr, + * NodeValueArray, etc.) + * + * The API mirrors the QVariant subset used by the node module, renamed to + * snake_case: value(), from_value(), can_convert(), is_null(), + * to_double(), to_float(), to_int(), to_uint(), to_long_long(), + * to_u_long_long(), to_bool(), to_string(), to_std_string(), + * to_string_list(), to_byte_array(). + * + * Conversion semantics follow QVariant for the stored kinds: numeric kinds + * convert freely between each other, strings parse to numbers (failure + * yields 0), and numbers format to strings (double uses QString::number's + * default 'g' format with 6 significant digits). Custom (type-erased) + * values only convert to their exact C++ type. + * + * Equality follows QVariant: two numeric values compare numerically across + * kinds, strings/byte-arrays/lists compare by content, custom values + * compare with the type's operator== (types without one compare unequal), + * and two nulls compare equal. + */ +class Variant { +public: + /** + * @brief Construct a null variant. + */ + Variant() = default; + + Variant(const Variant &other) + : kind_(other.kind_) + , int_(other.int_) + , uint_(other.uint_) + , double_(other.double_) + , bool_(other.bool_) + , string_(other.string_) + , string_list_(other.string_list_) + , byte_array_(other.byte_array_) + , custom_(other.custom_ ? other.custom_->clone() : nullptr) + { + } + + Variant(Variant &&) = default; + + Variant &operator=(const Variant &other) + { + if (this != &other) { + kind_ = other.kind_; + int_ = other.int_; + uint_ = other.uint_; + double_ = other.double_; + bool_ = other.bool_; + string_ = other.string_; + string_list_ = other.string_list_; + byte_array_ = other.byte_array_; + custom_.reset(other.custom_ ? other.custom_->clone() : nullptr); + } + return *this; + } + + Variant &operator=(Variant &&) = default; + + Variant(const char *s) + : kind_(k_string) + , string_(s) + { + } + + /** + * @brief String literal overload (beats the generic template ctor). + */ + template + Variant(const char (&s)[N]) + : kind_(k_string) + , string_(s) + { + } + + Variant(const std::string &s) + : kind_(k_string) + , string_(s) + { + } + + Variant(const StringList &l) + : kind_(k_string_list) + , string_list_(l) + { + } + + Variant(const ByteArray &b) + : kind_(k_byte_array) + , byte_array_(b) + { + } + + Variant(bool b) + : kind_(k_bool) + , bool_(b) + { + } + + /** + * @brief Construct from any other type. + * + * Arithmetic types are stored in the corresponding numeric kind + * (mirroring QVariant's int/uint/double storage); everything else is + * stored type-erased, mirroring QVariant::fromValue() of a custom + * metatype. + */ + template ::type, Variant>::value && + !std::is_same::type, bool>::value && + !std::is_same::type, std::string>::value && + !std::is_same::type, StringList>::value && + !std::is_same::type, ByteArray>::value, + int>::type = 0> + Variant(const T &v) + { + set_value(v); + } + + /** + * @brief QVariant::fromValue() equivalent. + */ + template static Variant from_value(const T &v) + { + return Variant(v); + } + + bool is_null() const + { + return kind_ == k_null; + } + + /** + * @brief Extract the value as type T. + * + * For the built-in kinds this performs QVariant-style conversion; for + * custom types it returns the stored value if the type matches exactly, + * or a default-constructed T otherwise (QVariant behavior). + */ + template T value() const + { + return value_impl(static_cast(nullptr)); + } + + /** + * @brief QVariant::canConvert() equivalent. + */ + template bool can_convert() const + { + return can_convert_impl(static_cast(nullptr)); + } + + double to_double(bool *ok = nullptr) const; + float to_float(bool *ok = nullptr) const; + int to_int(bool *ok = nullptr) const; + unsigned int to_uint(bool *ok = nullptr) const; + int64_t to_long_long(bool *ok = nullptr) const; + uint64_t to_u_long_long(bool *ok = nullptr) const; + bool to_bool() const; + + /** + * @brief QVariant::toString() equivalent (QString::number formatting for + * doubles: 'g' with 6 significant digits). + */ + std::string to_string() const; + + /** + * @brief QByteArray-like conversion of string data. + */ + std::string to_std_string() const + { + return to_string(); + } + + StringList to_string_list() const; + ByteArray to_byte_array() const; + + bool operator==(const Variant &rhs) const; + bool operator!=(const Variant &rhs) const + { + return !(*this == rhs); + } + +private: + enum Kind { + k_null, + k_bool, + k_int, + k_uint, + k_double, + k_string, + k_string_list, + k_byte_array, + k_custom + }; + + struct CustomHolderBase { + virtual ~CustomHolderBase() = default; + virtual CustomHolderBase *clone() const = 0; + virtual const std::type_info &type() const = 0; + virtual const void *data() const = 0; + virtual bool equals(const CustomHolderBase *rhs) const = 0; + }; + + template struct HasEquality : std::false_type { + }; + + template + struct HasEquality() == std::declval()), + bool>::value>::type> : std::true_type { + }; + + template bool custom_equals(const T &a, const T &b, std::true_type) const + { + return a == b; + } + + template bool custom_equals(const T &, const T &, std::false_type) const + { + return false; + } + + template struct CustomHolder : CustomHolderBase { + explicit CustomHolder(const T &v) + : value_(v) + { + } + + CustomHolderBase *clone() const override + { + return new CustomHolder(value_); + } + + const std::type_info &type() const override + { + return typeid(T); + } + + const void *data() const override + { + return &value_; + } + + bool equals(const CustomHolderBase *rhs) const override + { + if (rhs->type() != typeid(T)) { + return false; + } + const T &other = *static_cast(rhs->data()); + Variant dummy; + return dummy.custom_equals(value_, other, HasEquality()); + } + + T value_; + }; + + template + typename std::enable_if::value && std::is_signed::value && + !std::is_same::value, + void>::type + set_value(const T &v) + { + kind_ = k_int; + int_ = int64_t(v); + } + + template + typename std::enable_if::value && std::is_unsigned::value && + !std::is_same::value, + void>::type + set_value(const T &v) + { + kind_ = k_uint; + uint_ = uint64_t(v); + } + + template + typename std::enable_if::value, void>::type + set_value(const T &v) + { + kind_ = k_double; + double_ = double(v); + } + + template + typename std::enable_if< + !std::is_arithmetic::value && !std::is_same::value && + !std::is_same::value && !std::is_same::value && + !std::is_same::value, + void>::type + set_value(const T &v) + { + kind_ = k_custom; + custom_.reset(new CustomHolder(v)); + } + + bool is_numeric_kind() const + { + return kind_ == k_bool || kind_ == k_int || kind_ == k_uint || + kind_ == k_double; + } + + double numeric_as_double() const + { + switch (kind_) { + case k_bool: + return bool_ ? 1.0 : 0.0; + case k_int: + return double(int_); + case k_uint: + return double(uint_); + case k_double: + return double_; + default: + return 0.0; + } + } + + int64_t numeric_as_int64() const + { + switch (kind_) { + case k_bool: + return bool_ ? 1 : 0; + case k_int: + return int_; + case k_uint: + return int64_t(uint_); + case k_double: + return int64_t(double_); + default: + return 0; + } + } + + uint64_t numeric_as_uint64() const + { + switch (kind_) { + case k_bool: + return bool_ ? 1 : 0; + case k_int: + return uint64_t(int_); + case k_uint: + return uint_; + case k_double: + return uint64_t(double_); + default: + return 0; + } + } + + // value() dispatchers + + template + typename std::enable_if::value, T>::type + value_impl(T *) const + { + return to_string(); + } + + template + typename std::enable_if::value, T>::type + value_impl(T *) const + { + return to_string_list(); + } + + template + typename std::enable_if::value, T>::type + value_impl(T *) const + { + return to_byte_array(); + } + + template + typename std::enable_if::value, T>::type + value_impl(T *) const + { + return to_bool(); + } + + template + typename std::enable_if::value, T>::type + value_impl(T *) const + { + return T(to_double()); + } + + template + typename std::enable_if::value && std::is_signed::value && + !std::is_same::value, + T>::type + value_impl(T *) const + { + return T(to_long_long()); + } + + template + typename std::enable_if::value && std::is_unsigned::value && + !std::is_same::value, + T>::type + value_impl(T *) const + { + return T(to_u_long_long()); + } + + template + typename std::enable_if< + !std::is_arithmetic::value && !std::is_same::value && + !std::is_same::value && !std::is_same::value, + T>::type + value_impl(T *) const + { + if (kind_ == k_custom && custom_->type() == typeid(T)) { + return *static_cast(custom_->data()); + } + return T(); + } + + // can_convert() dispatchers + + template + typename std::enable_if::value, bool>::type + can_convert_impl(T *) const + { + return is_numeric_kind() || kind_ == k_string || kind_ == k_byte_array; + } + + template + typename std::enable_if::value, bool>::type + can_convert_impl(T *) const + { + return kind_ == k_string_list || kind_ == k_string; + } + + template + typename std::enable_if::value, bool>::type + can_convert_impl(T *) const + { + return kind_ == k_byte_array || kind_ == k_string; + } + + template + typename std::enable_if::value, bool>::type + can_convert_impl(T *) const + { + return is_numeric_kind() || kind_ == k_string; + } + + template + typename std::enable_if< + !std::is_arithmetic::value && !std::is_same::value && + !std::is_same::value && !std::is_same::value, + bool>::type + can_convert_impl(T *) const + { + return kind_ == k_custom && custom_->type() == typeid(T); + } + + Kind kind_ = k_null; + + int64_t int_ = 0; + uint64_t uint_ = 0; + double double_ = 0.0; + bool bool_ = false; + std::string string_; + StringList string_list_; + ByteArray byte_array_; + std::unique_ptr custom_; +}; + +/** + * @brief QByteArray::toBase64() equivalent. + */ +std::string byte_array_to_base64(const ByteArray &data); + +/** + * @brief QByteArray::fromBase64() equivalent. + */ +ByteArray byte_array_from_base64(const std::string &text); + +} + +#endif // OAK_NODEVARIANT_H diff --git a/src/node/standalone/CMakeLists.txt b/src/node/standalone/CMakeLists.txt new file mode 100644 index 000000000..a7efbb99e --- /dev/null +++ b/src/node/standalone/CMakeLists.txt @@ -0,0 +1,91 @@ +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Standalone build driver for the oaknode module. Per the M3 ruling (裁决 A), +# references into the not-yet-split render/codec/timeline/audio/pluginSupport +# modules are allowed to dangle: those headers are provided by the transitional +# stubs in src/node/transition (the engine headers are still Qt-based), and +# symbols are left undefined via -undefined dynamic_lookup (macOS). +# +# Builds oaknode, its C API, and its tests against the real in-repo targets +# (olivecore, ffmpeg_bridge, oakcommon, oakundo) instead of prebuilt dylibs. +# +# Usage (macOS/Homebrew): +# cmake -S src/node/standalone -B build-oaknode +# cmake --build build-oaknode -j +# ctest --test-dir build-oaknode + +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +project(oaknode-standalone LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) + +list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake") +if(EXISTS "/opt/homebrew") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") +endif() + +# Third-party dependencies. As in src/common/standalone, use the upstream +# CMake config packages shipped by Homebrew and map them onto the variables +# oakcommon's CMakeLists consumes. +find_package(EXPAT REQUIRED) +find_package(OpenColorIO CONFIG REQUIRED) +find_package(OpenImageIO CONFIG REQUIRED) + +set(OCIO_LIBRARIES OpenColorIO::OpenColorIO) +set(OCIO_INCLUDE_DIRS "") +set(OIIO_LIBRARIES OpenImageIO::OpenImageIO) +set(OIIO_INCLUDE_DIRS "") + +# In-repo libraries, built from source (real targets, not prebuilt dylibs): +# - olivecore (core/): oakcore_* C ABI, output name liboakcore +# - ffmpeg_bridge: fb_* C ABI used by oakcommon +# - oakundo: UndoCommand/UndoStack used by the oaknode C API +# - oakcommon: XmlStreamReader/Writer, Current, misc utils +set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE) +# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that +# break when linked from a build tree; oaknode does not need OTIO. +set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON) + +add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core) +# The top-level project puts third_party/openfx/include on the global +# include path; replicate it here for olivecore's pixelformat.h (ofxCore.h). +target_include_directories(olivecore PUBLIC ${OAK_REPO_ROOT}/third_party/openfx/include) +add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge) + +set(BUILD_TESTS OFF) +add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo) +add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common) +set(BUILD_TESTS ON) + +enable_testing() +add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node) + +# The transition stub dir must precede ${OAK_REPO_ROOT}/include so that e.g. +# "common/current.h" resolves to the transitional forwarder, not the stale +# public mirror in include/common. +target_include_directories(oaknode BEFORE PUBLIC + ${OAK_REPO_ROOT}/src/node/transition +) + +target_include_directories(oaknode PUBLIC + ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include + /opt/homebrew/include + /opt/homebrew/include/Imath +) + +# Symbols of the not-yet-split engine modules dangle; the test binary +# resolves them by linking the real targets and force-loading libOfxHost. +target_link_options(oaknode PRIVATE + "-undefined" "dynamic_lookup" +) diff --git a/src/node/tests/CMakeLists.txt b/src/node/tests/CMakeLists.txt new file mode 100644 index 000000000..421e11954 --- /dev/null +++ b/src/node/tests/CMakeLists.txt @@ -0,0 +1,73 @@ +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +find_package(GTest REQUIRED) +include(GoogleTest) + +# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone +# build (see src/node/standalone) sets OAK_REPO_ROOT explicitly. +if(NOT DEFINED OAK_REPO_ROOT) + set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR}) +endif() + +add_executable(oaknode-gtest + block_test.cpp + colormanager_test.cpp + factory_test.cpp + folder_test.cpp + footage_test.cpp + group_test.cpp + keyframe_test.cpp + node_test.cpp + project_test.cpp + sequence_test.cpp + serializer_test.cpp + track_test.cpp + traverser_test.cpp +) + +target_link_libraries(oaknode-gtest PRIVATE + oaknode + oakundo + oakcommon + olivecore + GTest::gtest + GTest::gtest_main +) + +# liboaknode dangles OFX host symbols (-undefined dynamic_lookup); force-load +# the host support archive into the test process so dyld finds them (and the +# OFX typeinfos) in the flat namespace at startup. Without this the binary +# crashes on launch, which also breaks gtest_discover_tests PRE_TEST +# discovery. The archive is a prebuilt artifact of the full-tree build +# (third_party/openfx); the standalone driver sets OAKNODE_OFX_HOST_ARCHIVE. +if(NOT DEFINED OAKNODE_OFX_HOST_ARCHIVE) + find_library(OAKNODE_OFX_HOST_ARCHIVE NAMES OfxHost + PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport) +endif() +if(NOT OAKNODE_OFX_HOST_ARCHIVE) + message(FATAL_ERROR + "libOfxHost.a not found; run the full-tree build once or set " + "OAKNODE_OFX_HOST_ARCHIVE") +endif() +target_link_options(oaknode-gtest PRIVATE + "-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}") + +# ColorManager builds its default OCIO config at Project construction time; +# point at the engine's bundled config (the de-Qt'd ":/ocioconf" extraction +# is a Qt-resource leftover that always fails). colormanager_test.cpp reads +# OAK_OCIO_TEST_CONFIG; the OCIO environment variable is needed both at +# ctest time and during PRE_TEST discovery (binary startup). +target_compile_definitions(oaknode-gtest PRIVATE + OAK_OCIO_TEST_CONFIG="${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio") + +gtest_discover_tests(oaknode-gtest + DISCOVERY_MODE PRE_TEST + PROPERTIES ENVIRONMENT + "OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio" +) diff --git a/src/node/tests/block_test.cpp b/src/node/tests/block_test.cpp new file mode 100644 index 000000000..34ff06c55 --- /dev/null +++ b/src/node/tests/block_test.cpp @@ -0,0 +1,307 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "common/loopmode.h" +#include "node/block.h" +#include "node/node.h" +#include "node/error.h" +#include "node/track.h" + +namespace +{ + +void expect_rational(int num, int den, int expected_num, int expected_den) +{ + EXPECT_EQ(num, expected_num); + EXPECT_EQ(den, expected_den); +} + +} // namespace + +TEST(BlockTest, CreateFreeClip) +{ + int base = oaknode_debug_alive_count(); + OakNodeBlock *clip = oaknode_block_clip_create(); + ASSERT_NE(clip, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), base + 1); + oaknode_block_free(clip); + EXPECT_EQ(oaknode_debug_alive_count(), base); +} + +TEST(BlockTest, CreateFreeGap) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + oaknode_block_free(gap); +} + +TEST(BlockTest, CreateTransitions) +{ + OakNodeBlock *cd = + oaknode_block_transition_create(OAKNODE_TRANSITION_CROSS_DISSOLVE); + EXPECT_NE(cd, nullptr); + OakNodeBlock *dc = + oaknode_block_transition_create(OAKNODE_TRANSITION_DIP_TO_COLOR); + EXPECT_NE(dc, nullptr); + EXPECT_EQ(oaknode_block_transition_create(99), nullptr); + EXPECT_EQ(oaknode_block_transition_create(-1), nullptr); + oaknode_block_free(cd); + oaknode_block_free(dc); +} + +TEST(BlockTest, FreeNullIsNoOp) +{ + oaknode_block_free(nullptr); +} + +TEST(BlockTest, NullHandleReturnsInvalid) +{ + int num, den; + EXPECT_EQ(oaknode_block_get_in(nullptr, &num, &den), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_block_set_in(nullptr, 0, 1), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_block_get_length(nullptr, &num, &den), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_block_get_enabled(nullptr, &num), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_clip_get_speed(nullptr, nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_transition_is_dual(nullptr, &num), OAKNODE_E_INVALID); + + OakNodeBlock *clip = oaknode_block_clip_create(); + ASSERT_NE(clip, nullptr); + EXPECT_EQ(oaknode_block_get_in(clip, nullptr, &den), OAKNODE_E_INVALID); + oaknode_block_free(clip); +} + +TEST(BlockTest, InOutLength) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + + int num, den; + ASSERT_EQ(oaknode_block_set_length_and_media_out(gap, 2, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_block_get_length(gap, &num, &den), OAKNODE_OK); + expect_rational(num, den, 2, 1); + + // In/out are timeline positions: they only move once the block sits on + // a track (Track::update_in_out_from), so a trackless block stays 0 + ASSERT_EQ(oaknode_block_get_out(gap, &num, &den), OAKNODE_OK); + expect_rational(num, den, 0, 1); + + ASSERT_EQ(oaknode_block_set_in(gap, 1, 2), OAKNODE_OK); + ASSERT_EQ(oaknode_block_get_in(gap, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 2); + + ASSERT_EQ(oaknode_block_set_out(gap, 5, 2), OAKNODE_OK); + ASSERT_EQ(oaknode_block_get_out(gap, &num, &den), OAKNODE_OK); + expect_rational(num, den, 5, 2); + + ASSERT_EQ(oaknode_block_set_length_and_media_in(gap, 1, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_block_get_length(gap, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 1); + + oaknode_block_free(gap); +} + +TEST(BlockTest, EnabledFlag) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + + int enabled = 0; + ASSERT_EQ(oaknode_block_get_enabled(gap, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 1); + + ASSERT_EQ(oaknode_block_set_enabled(gap, 0), OAKNODE_OK); + ASSERT_EQ(oaknode_block_get_enabled(gap, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 0); + + oaknode_block_free(gap); +} + +TEST(BlockTest, TracklessBlockHasNoNeighbours) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + + OakNodeBlock *neighbour = reinterpret_cast(0x1); + OakNodeTrack *track = reinterpret_cast(0x1); + ASSERT_EQ(oaknode_block_get_previous(gap, &neighbour), OAKNODE_OK); + EXPECT_EQ(neighbour, nullptr); + ASSERT_EQ(oaknode_block_get_next(gap, &neighbour), OAKNODE_OK); + EXPECT_EQ(neighbour, nullptr); + ASSERT_EQ(oaknode_block_get_track(gap, &track), OAKNODE_OK); + EXPECT_EQ(track, nullptr); + + oaknode_block_free(gap); +} + +TEST(BlockTest, LinkUnlink) +{ + OakNodeBlock *a = oaknode_block_clip_create(); + OakNodeBlock *b = oaknode_block_clip_create(); + ASSERT_NE(a, nullptr); + ASSERT_NE(b, nullptr); + + int linked = -1; + ASSERT_EQ(oaknode_block_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 0); + + ASSERT_EQ(oaknode_block_link(a, b), OAKNODE_OK); + // Double link fails + EXPECT_EQ(oaknode_block_link(a, b), OAKNODE_E_FAILED); + + ASSERT_EQ(oaknode_block_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 1); + + int count = 0; + ASSERT_EQ(oaknode_block_get_link_count(a, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + + OakNodeBlock *other = nullptr; + ASSERT_EQ(oaknode_block_get_link_at(a, 0, &other), OAKNODE_OK); + EXPECT_EQ(other, b); + EXPECT_EQ(oaknode_block_get_link_at(a, 1, &other), OAKNODE_E_NOT_FOUND); + + ASSERT_EQ(oaknode_block_unlink(a, b), OAKNODE_OK); + EXPECT_EQ(oaknode_block_unlink(a, b), OAKNODE_E_FAILED); + ASSERT_EQ(oaknode_block_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 0); + + EXPECT_EQ(oaknode_block_link(a, nullptr), OAKNODE_E_INVALID); + + oaknode_block_free(a); + oaknode_block_free(b); +} + +TEST(BlockTest, ClipProperties) +{ + OakNodeBlock *clip = oaknode_block_clip_create(); + ASSERT_NE(clip, nullptr); + + int num, den; + ASSERT_EQ(oaknode_clip_set_media_in(clip, 3, 2), OAKNODE_OK); + ASSERT_EQ(oaknode_clip_get_media_in(clip, &num, &den), OAKNODE_OK); + expect_rational(num, den, 3, 2); + + double speed = 0.0; + ASSERT_EQ(oaknode_clip_get_speed(clip, &speed), OAKNODE_OK); + EXPECT_DOUBLE_EQ(speed, 1.0); + ASSERT_EQ(oaknode_clip_set_speed(clip, 2.5), OAKNODE_OK); + ASSERT_EQ(oaknode_clip_get_speed(clip, &speed), OAKNODE_OK); + EXPECT_DOUBLE_EQ(speed, 2.5); + + int flag = -1; + ASSERT_EQ(oaknode_clip_get_reverse(clip, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 0); + ASSERT_EQ(oaknode_clip_set_reverse(clip, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_clip_get_reverse(clip, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 1); + + flag = -1; + ASSERT_EQ(oaknode_clip_get_maintain_audio_pitch(clip, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 0); + ASSERT_EQ(oaknode_clip_set_maintain_audio_pitch(clip, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_clip_get_maintain_audio_pitch(clip, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 1); + + int loop = -1; + ASSERT_EQ(oaknode_clip_get_loop_mode(clip, &loop), OAKNODE_OK); + EXPECT_EQ(loop, OAKCOMMON_LOOP_MODE_OFF); + ASSERT_EQ(oaknode_clip_set_loop_mode(clip, OAKCOMMON_LOOP_MODE_CLAMP), + OAKNODE_OK); + ASSERT_EQ(oaknode_clip_get_loop_mode(clip, &loop), OAKNODE_OK); + EXPECT_EQ(loop, OAKCOMMON_LOOP_MODE_CLAMP); + + // Trackless clip reports no track type + int type = 99; + ASSERT_EQ(oaknode_clip_get_track_type(clip, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_TRACK_TYPE_NONE); + + oaknode_block_free(clip); +} + +TEST(BlockTest, ClipApiRejectsNonClip) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + + double speed; + int num, den; + EXPECT_EQ(oaknode_clip_get_speed(gap, &speed), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_clip_set_media_in(gap, 1, 1), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_clip_get_media_in(gap, &num, &den), OAKNODE_E_INVALID); + + oaknode_block_free(gap); +} + +TEST(BlockTest, TransitionOffsets) +{ + OakNodeBlock *t = + oaknode_block_transition_create(OAKNODE_TRANSITION_CROSS_DISSOLVE); + ASSERT_NE(t, nullptr); + + int num, den; + ASSERT_EQ(oaknode_transition_set_offsets_and_length(t, 1, 2, 1, 2), + OAKNODE_OK); + // Length is the sum of both offsets + ASSERT_EQ(oaknode_block_get_length(t, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 1); + + // Unconnected transitions report zero offsets; the center is + // len/2 - in_offset = 0 here + ASSERT_EQ(oaknode_transition_get_in_offset(t, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 0); + ASSERT_EQ(oaknode_transition_get_out_offset(t, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 0); + ASSERT_EQ(oaknode_transition_get_offset_center(t, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 0); + + ASSERT_EQ(oaknode_transition_set_offset_center(t, 1, 4), OAKNODE_OK); + ASSERT_EQ(oaknode_transition_get_offset_center(t, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 4); + + int dual = -1; + ASSERT_EQ(oaknode_transition_is_dual(t, &dual), OAKNODE_OK); + EXPECT_EQ(dual, 0); + + OakNodeBlock *connected = reinterpret_cast(0x1); + ASSERT_EQ(oaknode_transition_get_connected_out_block(t, &connected), + OAKNODE_OK); + EXPECT_EQ(connected, nullptr); + ASSERT_EQ(oaknode_transition_get_connected_in_block(t, &connected), + OAKNODE_OK); + EXPECT_EQ(connected, nullptr); + + oaknode_block_free(t); +} + +TEST(BlockTest, TransitionApiRejectsNonTransition) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + + int num, den; + EXPECT_EQ(oaknode_transition_get_in_offset(gap, &num, &den), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_transition_set_offset_center(gap, 1, 2), + OAKNODE_E_INVALID); + + oaknode_block_free(gap); +} diff --git a/src/node/tests/colormanager_test.cpp b/src/node/tests/colormanager_test.cpp new file mode 100644 index 000000000..8a0d58cea --- /dev/null +++ b/src/node/tests/colormanager_test.cpp @@ -0,0 +1,298 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include +#include +#include +#include + +#include + +#include "node/colormanager.h" +#include "node/node.h" +#include "node/error.h" + +// Project C API belongs to another family; the tests drive the native +// object directly as scaffolding (borrowed OakNodeProject handle). +#include "project.h" + +#ifndef OAK_OCIO_TEST_CONFIG +#define OAK_OCIO_TEST_CONFIG "" +#endif + +namespace +{ + +/** + * @brief Fixture ensuring the process-wide default OCIO config resolves + * + * olive::Project's constructor initializes its own ColorManager, which + * builds the default config; point OCIO at the repository's bundled + * config so that succeeds without the Qt resource extraction path. + */ +class ColorManagerTest : public ::testing::Test { +protected: + static void SetUpTestSuite() + { + if (std::strlen(OAK_OCIO_TEST_CONFIG) > 0) { + setenv("OCIO", OAK_OCIO_TEST_CONFIG, 1); + } + } + + static std::string get_string(int (*fn)(OakNodeColorManager *, char *, + int), + OakNodeColorManager *m) + { + int needed = fn(m, nullptr, 0); + EXPECT_GT(needed, 0); + std::vector buf(needed); + EXPECT_EQ(fn(m, buf.data(), needed), needed); + return std::string(buf.data()); + } +}; + +} // namespace + +TEST_F(ColorManagerTest, InitFree) +{ + olive::Project project; + + int base = oaknode_debug_alive_count(); + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), base + 1); + oaknode_colormanager_free(m); + EXPECT_EQ(oaknode_debug_alive_count(), base); + + EXPECT_EQ(oaknode_colormanager_init(nullptr), nullptr); + oaknode_colormanager_free(nullptr); +} + +TEST_F(ColorManagerTest, NullHandleReturnsInvalid) +{ + char buf[64]; + int count; + EXPECT_EQ(oaknode_colormanager_get_config_filename(nullptr, buf, + sizeof(buf)), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_colormanager_initialize(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_colormanager_get_display_count(nullptr, &count), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_colormanager_set_config_filename(nullptr, "x"), + OAKNODE_E_INVALID); +} + +TEST_F(ColorManagerTest, ConfigFilenameRoundTrip) +{ + olive::Project project; + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + + ASSERT_EQ(oaknode_colormanager_set_config_filename(m, "/tmp/myconfig.ocio"), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_colormanager_get_config_filename, m), + "/tmp/myconfig.ocio"); + + EXPECT_EQ(oaknode_colormanager_set_config_filename(m, nullptr), + OAKNODE_E_INVALID); + + oaknode_colormanager_free(m); +} + +TEST_F(ColorManagerTest, ProjectBackedColorSpaces) +{ + olive::Project project; + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + + // Stored on the project: works even before a config is attached + ASSERT_EQ(oaknode_colormanager_set_default_input_color_space(m, "Linear"), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_colormanager_get_default_input_color_space, + m), + "Linear"); + + // The Project constructor initialized its own manager with the scene + // linear role as reference space + std::string ref = + get_string(oaknode_colormanager_get_reference_color_space, m); + EXPECT_FALSE(ref.empty()); + + oaknode_colormanager_free(m); +} + +TEST_F(ColorManagerTest, ConfigDependentCallsRequireConfig) +{ + olive::Project project; + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + + // This manager never got initialize(): no config attached + int count; + char buf[64]; + double rgb[3]; + EXPECT_EQ(oaknode_colormanager_get_colorspace_count(m, &count), + OAKNODE_E_STATE); + EXPECT_EQ(oaknode_colormanager_get_display_count(m, &count), + OAKNODE_E_STATE); + EXPECT_EQ(oaknode_colormanager_get_default_display(m, buf, sizeof(buf)), + OAKNODE_E_STATE); + EXPECT_EQ(oaknode_colormanager_get_default_luma_coefs(m, rgb), + OAKNODE_E_STATE); + EXPECT_EQ(oaknode_colormanager_get_compliant_color_space(m, "x", buf, + sizeof(buf)), + OAKNODE_E_STATE); + EXPECT_EQ(oaknode_colormanager_get_colorspace_for_ffmpeg_tags(m, 1, 1, buf, + sizeof(buf)), + OAKNODE_E_STATE); + + oaknode_colormanager_free(m); +} + +TEST_F(ColorManagerTest, InitializeAndQueryConfig) +{ + olive::Project project; + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + + ASSERT_EQ(oaknode_colormanager_initialize(m), OAKNODE_OK); + + int count = 0; + ASSERT_EQ(oaknode_colormanager_get_colorspace_count(m, &count), OAKNODE_OK); + ASSERT_GT(count, 0); + + int needed = oaknode_colormanager_get_colorspace_at(m, 0, nullptr, 0); + ASSERT_GT(needed, 1); + std::vector buf(needed); + ASSERT_EQ(oaknode_colormanager_get_colorspace_at(m, 0, buf.data(), needed), + needed); + EXPECT_GT(std::strlen(buf.data()), 0U); + EXPECT_EQ(oaknode_colormanager_get_colorspace_at(m, count, buf.data(), + needed), + OAKNODE_E_NOT_FOUND); + + int displays = 0; + ASSERT_EQ(oaknode_colormanager_get_display_count(m, &displays), OAKNODE_OK); + ASSERT_GT(displays, 0); + std::string display = + get_string(oaknode_colormanager_get_default_display, m); + EXPECT_FALSE(display.empty()); + + int views = 0; + ASSERT_EQ(oaknode_colormanager_get_view_count(m, display.c_str(), &views), + OAKNODE_OK); + ASSERT_GT(views, 0); + std::string view; + { + int vneeded = + oaknode_colormanager_get_default_view(m, display.c_str(), nullptr, 0); + ASSERT_GT(vneeded, 0); + std::vector vbuf(vneeded); + ASSERT_EQ(oaknode_colormanager_get_default_view(m, display.c_str(), + vbuf.data(), vneeded), + vneeded); + view = vbuf.data(); + } + EXPECT_FALSE(view.empty()); + + int looks = -1; + ASSERT_EQ(oaknode_colormanager_get_look_count(m, &looks), OAKNODE_OK); + EXPECT_GE(looks, 0); + + double rgb[3] = { 0.0, 0.0, 0.0 }; + ASSERT_EQ(oaknode_colormanager_get_default_luma_coefs(m, rgb), OAKNODE_OK); + EXPECT_GT(rgb[0], 0.0); + EXPECT_GT(rgb[1], 0.0); + EXPECT_GT(rgb[2], 0.0); + + // Unknown tags map to the empty string (required size 1 = just NUL) + EXPECT_EQ(oaknode_colormanager_get_colorspace_for_ffmpeg_tags(m, 0, 0, + nullptr, 0), + 1); + + // A colorspace the config does not contain falls back to the default + std::string compliant; + { + int cneeded = oaknode_colormanager_get_compliant_color_space( + m, "NoSuchColorSpace", nullptr, 0); + ASSERT_GT(cneeded, 1); + std::vector cbuf(cneeded); + ASSERT_EQ(oaknode_colormanager_get_compliant_color_space( + m, "NoSuchColorSpace", cbuf.data(), cneeded), + cneeded); + compliant = cbuf.data(); + } + EXPECT_EQ(compliant, + get_string(oaknode_colormanager_get_default_input_color_space, + m)); + + oaknode_colormanager_free(m); +} + +TEST_F(ColorManagerTest, CompliantColorTransform) +{ + olive::Project project; + OakNodeColorManager *m = + oaknode_colormanager_init(reinterpret_cast(&project)); + ASSERT_NE(m, nullptr); + ASSERT_EQ(oaknode_colormanager_initialize(m), OAKNODE_OK); + + std::string display = + get_string(oaknode_colormanager_get_default_display, m); + ASSERT_FALSE(display.empty()); + + OakCommonColorTransform *t = oakcommon_colortransform_init_display( + display.c_str(), "No Such View", ""); + ASSERT_NE(t, nullptr); + + OakCommonColorTransform *compliant = nullptr; + ASSERT_EQ(oaknode_colormanager_get_compliant_color_transform(m, t, 0, + &compliant), + OAKNODE_OK); + ASSERT_NE(compliant, nullptr); + + int is_display = 0; + ASSERT_EQ(oakcommon_colortransform_is_display(compliant, &is_display), + OAKCOMMON_OK); + EXPECT_EQ(is_display, 1); + + // The bogus view was replaced by the config's default view + int needed = oakcommon_colortransform_get_view(compliant, nullptr, 0); + ASSERT_GT(needed, 0); + std::vector buf(needed); + ASSERT_EQ(oakcommon_colortransform_get_view(compliant, buf.data(), needed), + needed); + EXPECT_STRNE(buf.data(), "No Such View"); + + oakcommon_colortransform_free(compliant); + oakcommon_colortransform_free(t); + + // Error paths + EXPECT_EQ(oaknode_colormanager_get_compliant_color_transform(m, nullptr, 0, + &compliant), + OAKNODE_E_INVALID); + + oaknode_colormanager_free(m); +} diff --git a/src/node/tests/factory_test.cpp b/src/node/tests/factory_test.cpp new file mode 100644 index 000000000..b29499d57 --- /dev/null +++ b/src/node/tests/factory_test.cpp @@ -0,0 +1,149 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/factory.h" +#include "node/group.h" + +#include + +#include +#include + +#include "testnode.h" + +namespace +{ + +class NodeFactoryTest : public ::testing::Test { +protected: + void SetUp() override + { + ASSERT_EQ(oaknode_factory_initialize(), OAKNODE_OK); + } + + void TearDown() override + { + oaknode_factory_destroy(); + } +}; + +TEST_F(NodeFactoryTest, EnumerateLibrary) +{ + int count = 0; + EXPECT_EQ(oaknode_factory_id_count(&count), OAKNODE_OK); + EXPECT_GT(count, 0); + + // Every id is non-empty and name_from_id maps back to a name. + for (int i = 0; i < count; i++) { + int required = oaknode_factory_id_at(i, nullptr, 0); + ASSERT_GT(required, 1) << i; + std::vector id(static_cast(required)); + ASSERT_EQ(oaknode_factory_id_at(i, id.data(), required), required); + + int name_required = + oaknode_factory_name_from_id(id.data(), nullptr, 0); + EXPECT_GT(name_required, 1) << id.data(); + } + + EXPECT_EQ(oaknode_factory_id_at(count, nullptr, 0), OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_factory_id_at(-1, nullptr, 0), OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_factory_id_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_factory_name_from_id(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + + // Unknown ids yield an empty name (required size 1). + EXPECT_EQ(oaknode_factory_name_from_id("org.oak.DoesNotExist", nullptr, 0), + 1); +} + +TEST_F(NodeFactoryTest, NodeAt) +{ + int count = 0; + ASSERT_EQ(oaknode_factory_id_count(&count), OAKNODE_OK); + ASSERT_GT(count, 0); + + OakNodeNode *prototype = nullptr; + EXPECT_EQ(oaknode_factory_node_at(0, &prototype), OAKNODE_OK); + ASSERT_NE(prototype, nullptr); + + // The prototype's id matches id_at(0). + int required = oaknode_factory_id_at(0, nullptr, 0); + ASSERT_GT(required, 1); + std::vector id(static_cast(required)); + ASSERT_EQ(oaknode_factory_id_at(0, id.data(), required), required); + + char buf[256]; + ASSERT_GT(oaknode_node_get_id(prototype, buf, sizeof(buf)), 1); + EXPECT_STREQ(buf, id.data()); + + EXPECT_EQ(oaknode_factory_node_at(count, &prototype), OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_factory_node_at(0, nullptr), OAKNODE_E_INVALID); +} + +TEST_F(NodeFactoryTest, CreateFromId) +{ + // The group node is part of family A and always registered. + int alive_before = oaknode_debug_alive_count(); + + int name_required = + oaknode_factory_name_from_id("org.olivevideoeditor.Olive.group", + nullptr, 0); + ASSERT_GT(name_required, 1); + std::vector name(static_cast(name_required)); + EXPECT_EQ(oaknode_factory_name_from_id("org.olivevideoeditor.Olive.group", + name.data(), name_required), + name_required); + + OakNodeNode *node = + oaknode_factory_create_from_id("org.olivevideoeditor.Olive.group"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + char buf[256]; + ASSERT_GT(oaknode_node_get_id(node, buf, sizeof(buf)), 1); + EXPECT_STREQ(buf, "org.olivevideoeditor.Olive.group"); + + // The created instance is a group. + EXPECT_NE(oaknode_group_cast(node), nullptr); + + oaknode_node_free(node); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + + EXPECT_EQ(oaknode_factory_create_from_id("org.oak.DoesNotExist"), nullptr); + EXPECT_EQ(oaknode_factory_create_from_id(nullptr), nullptr); +} + +TEST(NodeFactoryUninitializedTest, StateErrors) +{ + // Ensure the library is empty regardless of test execution order. + oaknode_factory_destroy(); + + int count = 0; + EXPECT_EQ(oaknode_factory_id_count(&count), OAKNODE_E_STATE); + EXPECT_EQ(oaknode_factory_id_at(0, nullptr, 0), OAKNODE_E_STATE); + OakNodeNode *node = nullptr; + EXPECT_EQ(oaknode_factory_node_at(0, &node), OAKNODE_E_STATE); + + // Re-initialize for any subsequent test run. + EXPECT_EQ(oaknode_factory_initialize(), OAKNODE_OK); + oaknode_factory_destroy(); +} + +} diff --git a/src/node/tests/folder_test.cpp b/src/node/tests/folder_test.cpp new file mode 100644 index 000000000..51000be7b --- /dev/null +++ b/src/node/tests/folder_test.cpp @@ -0,0 +1,192 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "node/folder.h" +#include "node/project.h" + +namespace +{ + +OakNodeNode *as_node(OakNodeFolder *folder) +{ + return reinterpret_cast(folder); +} + +/** + * @brief Detach all folder edges before project teardown. + * + * WORKAROUND: Project::clear() calls set_parent(nullptr) on each node + * BEFORE deleting it, so Node::~Node() -> disconnect_all() trips the + * same-graph assertion in Node::disconnect_edge() for any remaining + * folder edges (upstream bug, reported to the parent agent). Removing + * the edges first keeps teardown clean. + */ +void detach_all(OakNodeFolder *root, OakNodeFolder *a, OakNodeFolder *b) +{ + if (oaknode_folder_index_of_child(root, as_node(a)) >= 0) { + oaknode_folder_remove_child(root, as_node(a)); + } + if (oaknode_folder_index_of_child(root, as_node(b)) >= 0) { + oaknode_folder_remove_child(root, as_node(b)); + } + if (oaknode_folder_index_of_child(a, as_node(b)) >= 0) { + oaknode_folder_remove_child(a, as_node(b)); + } +} + +} // namespace + +TEST(NodeFolder, CreateAndHierarchy) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + OakNodeFolder *root = oaknode_project_root(project); + ASSERT_NE(root, nullptr); + + // Creating a folder requires a project + EXPECT_EQ(oaknode_folder_create(nullptr), nullptr); + + OakNodeFolder *a = oaknode_folder_create(project); + OakNodeFolder *b = oaknode_folder_create(project); + ASSERT_NE(a, nullptr); + ASSERT_NE(b, nullptr); + + EXPECT_EQ(oaknode_folder_child_count(root), 0); + EXPECT_EQ(oaknode_folder_child_count(a), 0); + + // add_child builds the hierarchy + EXPECT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_child_count(root), 1); + EXPECT_EQ(oaknode_folder_child_at(root, 0), as_node(a)); + EXPECT_EQ(oaknode_folder_index_of_child(root, as_node(a)), 0); + EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), root); + + EXPECT_EQ(oaknode_folder_add_child(a, as_node(b)), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_has_child_recursive(root, as_node(b)), 1); + EXPECT_EQ(oaknode_folder_has_child_recursive(a, as_node(b)), 1); + EXPECT_EQ(oaknode_folder_has_child_recursive(b, as_node(a)), 0); + + // A node can only be in one folder at a time + EXPECT_EQ(oaknode_folder_add_child(root, as_node(b)), OAKNODE_E_STATE); + + // Out-of-range child access yields NULL + EXPECT_EQ(oaknode_folder_child_at(root, -1), nullptr); + EXPECT_EQ(oaknode_folder_child_at(root, 1), nullptr); + + // index_of_child on a non-child is E_NOT_FOUND + EXPECT_EQ(oaknode_folder_index_of_child(root, as_node(b)), + OAKNODE_E_NOT_FOUND); + + detach_all(root, a, b); + oaknode_project_free(project); +} + +TEST(NodeFolder, RemoveChild) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + OakNodeFolder *root = oaknode_project_root(project); + OakNodeFolder *a = oaknode_folder_create(project); + ASSERT_NE(root, nullptr); + ASSERT_NE(a, nullptr); + + ASSERT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_child_count(root), 0); + EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), nullptr); + + // Removing a non-child is E_NOT_FOUND + EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)), + OAKNODE_E_NOT_FOUND); + + // The folder survives removal and can be re-added + EXPECT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_child_count(root), 1); + + detach_all(root, a, nullptr); + oaknode_project_free(project); +} + +TEST(NodeFolder, MoveChildren) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + OakNodeFolder *root = oaknode_project_root(project); + OakNodeFolder *a = oaknode_folder_create(project); + OakNodeFolder *b = oaknode_folder_create(project); + OakNodeFolder *c = oaknode_folder_create(project); + ASSERT_NE(root, nullptr); + ASSERT_NE(a, nullptr); + ASSERT_NE(b, nullptr); + ASSERT_NE(c, nullptr); + + ASSERT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK); + ASSERT_EQ(oaknode_folder_add_child(root, as_node(b)), OAKNODE_OK); + ASSERT_EQ(oaknode_folder_add_child(a, as_node(c)), OAKNODE_OK); + + // Move b and c into a in one call + OakNodeNode *to_move[] = { as_node(b), as_node(c) }; + EXPECT_EQ(oaknode_folder_move_children(to_move, 2, a), OAKNODE_OK); + + EXPECT_EQ(oaknode_folder_child_count(root), 1); + // c was already in a and is skipped; a ends up with {c, b} + EXPECT_EQ(oaknode_folder_child_count(a), 2); + EXPECT_EQ(oaknode_folder_parent_of(as_node(b)), a); + EXPECT_EQ(oaknode_folder_parent_of(as_node(c)), a); + EXPECT_EQ(oaknode_folder_index_of_child(a, as_node(c)), 0); + EXPECT_EQ(oaknode_folder_index_of_child(a, as_node(b)), 1); + + // Moving a node already in the destination is a no-op + EXPECT_EQ(oaknode_folder_move_children(to_move, 2, a), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_child_count(a), 2); + + // A node with no folder is simply appended + EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)), OAKNODE_OK); + OakNodeNode *one[] = { as_node(a) }; + EXPECT_EQ(oaknode_folder_move_children(one, 1, root), OAKNODE_OK); + EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), root); + + // Detach every remaining edge before teardown (see detach_all). + detach_all(root, a, b); + detach_all(a, b, c); + detach_all(b, a, c); + oaknode_project_free(project); +} + +TEST(NodeFolder, NullHandleErrors) +{ + EXPECT_EQ(oaknode_folder_child_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_child_at(nullptr, 0), nullptr); + EXPECT_EQ(oaknode_folder_add_child(nullptr, nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_remove_child(nullptr, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_move_children(nullptr, 0, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_has_child_recursive(nullptr, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_index_of_child(nullptr, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_folder_parent_of(nullptr), nullptr); +} diff --git a/src/node/tests/footage_test.cpp b/src/node/tests/footage_test.cpp new file mode 100644 index 000000000..f20106261 --- /dev/null +++ b/src/node/tests/footage_test.cpp @@ -0,0 +1,172 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "node/footage.h" +#include "node/project.h" + +namespace +{ + +/** + * @brief Two-stage string getter helper: query, then fetch. + */ +std::string get_string(int (*fn)(const OakNodeFootage *, char *, int), + const OakNodeFootage *footage) +{ + int required = fn(footage, nullptr, 0); + if (required <= 0) { + return std::string(); + } + std::vector buf(static_cast(required)); + EXPECT_EQ(fn(footage, buf.data(), required), required); + return std::string(buf.data()); +} + +} // namespace + +TEST(NodeFootage, CreateAndFilename) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + // Creating footage requires a project + EXPECT_EQ(oaknode_footage_create(nullptr, nullptr), nullptr); + + OakNodeFootage *footage = oaknode_footage_create(project, nullptr); + ASSERT_NE(footage, nullptr); + EXPECT_EQ(get_string(oaknode_footage_filename, footage), ""); + + // A nonexistent path keeps the footage invalid but stores the name + // (no probe is triggered for missing files). + EXPECT_EQ(oaknode_footage_set_filename( + footage, "/nonexistent/dir/clip.mp4"), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_footage_filename, footage), + "/nonexistent/dir/clip.mp4"); + EXPECT_EQ(oaknode_footage_is_valid(footage), 0); + + EXPECT_EQ(oaknode_footage_set_filename(footage, nullptr), + OAKNODE_E_INVALID); + + oaknode_project_free(project); +} + +TEST(NodeFootage, TimestampAndMetadataDefaults) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + OakNodeFootage *footage = oaknode_footage_create(project, nullptr); + ASSERT_NE(footage, nullptr); + + int64_t timestamp = -1; + EXPECT_EQ(oaknode_footage_timestamp(footage, ×tamp), OAKNODE_OK); + EXPECT_EQ(timestamp, 0); + + EXPECT_EQ(oaknode_footage_set_timestamp(footage, 123456789), OAKNODE_OK); + EXPECT_EQ(oaknode_footage_timestamp(footage, ×tamp), OAKNODE_OK); + EXPECT_EQ(timestamp, 123456789); + + // Unprobed footage has no decoder and no streams + EXPECT_EQ(get_string(oaknode_footage_decoder, footage), ""); + EXPECT_EQ(oaknode_footage_total_stream_count(footage), 0); + EXPECT_EQ(oaknode_footage_video_stream_count(footage), 0); + EXPECT_EQ(oaknode_footage_audio_stream_count(footage), 0); + EXPECT_EQ(oaknode_footage_subtitle_stream_count(footage), 0); + + int numerator = -1; + int denominator = -1; + EXPECT_EQ(oaknode_footage_duration(footage, &numerator, &denominator), + OAKNODE_OK); + EXPECT_EQ(numerator, 0); + + oaknode_project_free(project); +} + +TEST(NodeFootage, Proxy) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + OakNodeFootage *footage = oaknode_footage_create(project, nullptr); + ASSERT_NE(footage, nullptr); + + EXPECT_EQ(oaknode_footage_proxy_enabled(footage), 0); + EXPECT_EQ(get_string(oaknode_footage_proxy_path, footage), ""); + // ProxyManager::k_proxy_missing + EXPECT_EQ(oaknode_footage_proxy_state(footage), 0); + + EXPECT_EQ(oaknode_footage_set_proxy_enabled(footage, 1), OAKNODE_OK); + EXPECT_EQ(oaknode_footage_proxy_enabled(footage), 1); + + // ProxyManager::k_proxy_ready == 2 + EXPECT_EQ(oaknode_footage_set_proxy(footage, "/tmp/proxy.mov", 2, 0, 1, 1), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_footage_proxy_path, footage), + "/tmp/proxy.mov"); + EXPECT_EQ(oaknode_footage_proxy_state(footage), 2); + EXPECT_EQ(oaknode_footage_proxy_enabled(footage), 1); + + EXPECT_EQ(oaknode_footage_clear_proxy(footage), OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_footage_proxy_path, footage), ""); + EXPECT_EQ(oaknode_footage_proxy_state(footage), 0); + + oaknode_project_free(project); +} + +TEST(NodeFootage, NullHandleErrors) +{ + int64_t timestamp = 0; + int num = 0; + int den = 0; + + EXPECT_EQ(oaknode_footage_filename(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_set_filename(nullptr, "/tmp/x"), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_is_valid(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_timestamp(nullptr, ×tamp), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_timestamp(nullptr, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_set_timestamp(nullptr, 0), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_decoder(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_total_stream_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_video_stream_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_audio_stream_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_subtitle_stream_count(nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_duration(nullptr, &num, &den), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_proxy_enabled(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_set_proxy_enabled(nullptr, 1), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_proxy_path(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_proxy_state(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_set_proxy(nullptr, "/tmp/p", 2, 0, 1, 1), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_footage_clear_proxy(nullptr), OAKNODE_E_INVALID); +} diff --git a/src/node/tests/group_test.cpp b/src/node/tests/group_test.cpp new file mode 100644 index 000000000..58ec111f5 --- /dev/null +++ b/src/node/tests/group_test.cpp @@ -0,0 +1,246 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/group.h" + +#include + +#include + +#include "undo/undocommand.h" + +#include "testnode.h" + +namespace +{ + +using oaknode_test::TestNode; +using oaknode_test::as_handle; + +/** + * @brief Create a group with `inner` registered in its context + * (NodeGroup::add_input_passthrough() asserts context membership). + */ +OakNodeGroup *make_group_with_inner(OakNodeNode *inner) +{ + OakNodeGroup *group = oaknode_group_create(); + EXPECT_NE(group, nullptr); + EXPECT_EQ(oaknode_node_set_context_position( + reinterpret_cast(group), inner, 0.0, 0.0, 0), + OAKNODE_OK); + return group; +} + +TEST(NodeGroupTest, CreateCastFree) +{ + int alive_before = oaknode_debug_alive_count(); + + OakNodeGroup *group = oaknode_group_create(); + ASSERT_NE(group, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + OakNodeNode *as_node = reinterpret_cast(group); + EXPECT_EQ(oaknode_group_cast(as_node), group); + + TestNode plain; + EXPECT_EQ(oaknode_group_cast(as_handle(&plain)), nullptr); + EXPECT_EQ(oaknode_group_cast(nullptr), nullptr); + + oaknode_group_free(group); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + + oaknode_group_free(nullptr); // no crash +} + +TEST(NodeGroupTest, PassthroughAddEnumerateRemove) +{ + TestNode inner; + OakNodeNode *inner_handle = as_handle(&inner); + OakNodeGroup *group = make_group_with_inner(inner_handle); + ASSERT_NE(group, nullptr); + OakNodeNode *group_node = reinterpret_cast(group); + + // Two-stage: query the generated id size first. + int float_required = oaknode_group_add_input_passthrough( + group, inner_handle, "float_in", -1, nullptr, 0); + EXPECT_EQ(float_required, 9); // "float_in" + char int_id[16]; + int int_required = oaknode_group_add_input_passthrough( + group, inner_handle, "int_in", -1, int_id, sizeof(int_id)); + EXPECT_EQ(int_required, 7); // "int_in" + EXPECT_STREQ(int_id, "int_in"); + + int count = 0; + EXPECT_EQ(oaknode_group_passthrough_count(group, &count), OAKNODE_OK); + EXPECT_EQ(count, 2); + + char buf[64]; + EXPECT_EQ(oaknode_group_passthrough_id_at(group, 0, buf, sizeof(buf)), 9); + EXPECT_STREQ(buf, "float_in"); + EXPECT_EQ(oaknode_group_passthrough_id_at(group, 2, buf, sizeof(buf)), + OAKNODE_E_NOT_FOUND); + + // The passthrough becomes an input of the group. + int type = -1; + EXPECT_EQ(oaknode_node_input_get_type(group_node, buf, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_VALUE_FLOAT); + + OakNodeNode *pt_node = nullptr; + int pt_element = -2; + char pt_id[64]; + EXPECT_EQ(oaknode_group_passthrough_input_at(group, 0, &pt_node, pt_id, + sizeof(pt_id), &pt_element), + 9); + EXPECT_EQ(pt_node, inner_handle); + EXPECT_STREQ(pt_id, "float_in"); + EXPECT_EQ(pt_element, -1); + EXPECT_EQ(oaknode_group_passthrough_input_at(group, 5, &pt_node, pt_id, + sizeof(pt_id), &pt_element), + OAKNODE_E_NOT_FOUND); + + EXPECT_EQ(oaknode_group_remove_input_passthrough(group, inner_handle, + "float_in", -1), + OAKNODE_OK); + EXPECT_EQ(oaknode_group_passthrough_count(group, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + EXPECT_EQ(oaknode_group_remove_input_passthrough(group, inner_handle, + "float_in", -1), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_group_remove_input_passthrough(nullptr, inner_handle, + "float_in", -1), + OAKNODE_E_INVALID); + + oaknode_group_free(group); +} + +TEST(NodeGroupTest, PassthroughAddUndoable) +{ + TestNode inner; + OakNodeGroup *group = make_group_with_inner(as_handle(&inner)); + ASSERT_NE(group, nullptr); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_group_add_input_passthrough_undoable( + group, as_handle(&inner), "float_in", -1, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + + int count = 0; + EXPECT_EQ(oaknode_group_passthrough_count(group, &count), OAKNODE_OK); + EXPECT_EQ(count, 0); + + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_group_passthrough_count(group, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_group_passthrough_count(group, &count), OAKNODE_OK); + EXPECT_EQ(count, 0); + + oakundo_command_free(command); + oaknode_group_free(group); +} + +TEST(NodeGroupTest, OutputPassthroughLiveAndUndoable) +{ + TestNode inner; + OakNodeNode *inner_handle = as_handle(&inner); + OakNodeGroup *group = make_group_with_inner(inner_handle); + ASSERT_NE(group, nullptr); + + OakNodeNode *out = reinterpret_cast(uintptr_t(1)); + EXPECT_EQ(oaknode_group_get_output_passthrough(group, &out), OAKNODE_OK); + EXPECT_EQ(out, nullptr); + + EXPECT_EQ(oaknode_group_set_output_passthrough(group, inner_handle), + OAKNODE_OK); + EXPECT_EQ(oaknode_group_get_output_passthrough(group, &out), OAKNODE_OK); + EXPECT_EQ(out, inner_handle); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_group_set_output_passthrough_undoable(group, nullptr, + &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_group_get_output_passthrough(group, &out), OAKNODE_OK); + EXPECT_EQ(out, nullptr); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_group_get_output_passthrough(group, &out), OAKNODE_OK); + EXPECT_EQ(out, inner_handle); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_group_set_output_passthrough(nullptr, inner_handle), + OAKNODE_E_INVALID); + + oaknode_group_free(group); +} + +TEST(NodeGroupTest, ResolveInput) +{ + TestNode inner; + OakNodeNode *inner_handle = as_handle(&inner); + OakNodeGroup *group = make_group_with_inner(inner_handle); + ASSERT_NE(group, nullptr); + OakNodeNode *group_node = reinterpret_cast(group); + + char id_buf[64]; + int required = oaknode_group_add_input_passthrough(group, inner_handle, + "float_in", -1, id_buf, + sizeof(id_buf)); + ASSERT_GT(required, 1); + + // Resolving the group's passthrough id yields the inner input. + OakNodeNode *resolved_node = nullptr; + int resolved_element = -2; + char resolved_id[64]; + EXPECT_EQ(oaknode_group_resolve_input(group_node, id_buf, -1, + &resolved_node, resolved_id, + sizeof(resolved_id), + &resolved_element), + 9); + EXPECT_EQ(resolved_node, inner_handle); + EXPECT_STREQ(resolved_id, "float_in"); + EXPECT_EQ(resolved_element, -1); + + // A non-group input resolves to itself. + EXPECT_EQ(oaknode_group_resolve_input(inner_handle, "float_in", -1, + &resolved_node, resolved_id, + sizeof(resolved_id), + &resolved_element), + 9); + EXPECT_EQ(resolved_node, inner_handle); + EXPECT_STREQ(resolved_id, "float_in"); + + // Error paths. + EXPECT_EQ(oaknode_group_resolve_input(nullptr, id_buf, -1, &resolved_node, + resolved_id, sizeof(resolved_id), + &resolved_element), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_group_resolve_input(group_node, nullptr, -1, + &resolved_node, resolved_id, + sizeof(resolved_id), + &resolved_element), + OAKNODE_E_INVALID); + + oaknode_group_free(group); +} + +} diff --git a/src/node/tests/keyframe_test.cpp b/src/node/tests/keyframe_test.cpp new file mode 100644 index 000000000..0d4e77506 --- /dev/null +++ b/src/node/tests/keyframe_test.cpp @@ -0,0 +1,298 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/keyframe.h" + +#include + +#include "undo/undocommand.h" + +#include "testnode.h" + +namespace +{ + +using oaknode_test::TestNode; +using oaknode_test::as_handle; + +oaknode_value make_float(double f) +{ + oaknode_value v = oaknode_value(); + v.type = OAKNODE_VALUE_FLOAT; + v.f[0] = f; + return v; +} + +TEST(NodeKeyframeTest, EnumOrdinalsArePinned) +{ + EXPECT_EQ(OAKNODE_KEYFRAME_INVALID, -1); + EXPECT_EQ(OAKNODE_KEYFRAME_LINEAR, 0); + EXPECT_EQ(OAKNODE_KEYFRAME_HOLD, 1); + EXPECT_EQ(OAKNODE_KEYFRAME_BEZIER, 2); + EXPECT_EQ(OAKNODE_KEYFRAME_IN_HANDLE, 0); + EXPECT_EQ(OAKNODE_KEYFRAME_OUT_HANDLE, 1); +} + +TEST(NodeKeyframeTest, CreateAccessorsFree) +{ + TestNode node; + OakNodeNode *node_handle = as_handle(&node); + + int alive_before = oaknode_debug_alive_count(); + + oaknode_value value = make_float(1.5); + OakNodeKeyframe *key = oaknode_keyframe_create(2, 1, &value, + OAKNODE_KEYFRAME_LINEAR, 0, + -1, "float_in", + node_handle); + ASSERT_NE(key, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + int64_t num = 0, den = 0; + EXPECT_EQ(oaknode_keyframe_get_time(key, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 2); + EXPECT_EQ(den, 1); + + // The parent node's declared type pins the value mapping. + oaknode_value out; + EXPECT_EQ(oaknode_keyframe_get_value(key, &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_FLOAT); + EXPECT_DOUBLE_EQ(out.f[0], 1.5); + + int scalar = -1; + EXPECT_EQ(oaknode_keyframe_get_type(key, &scalar), OAKNODE_OK); + EXPECT_EQ(scalar, OAKNODE_KEYFRAME_LINEAR); + EXPECT_EQ(oaknode_keyframe_get_track(key, &scalar), OAKNODE_OK); + EXPECT_EQ(scalar, 0); + EXPECT_EQ(oaknode_keyframe_get_element(key, &scalar), OAKNODE_OK); + EXPECT_EQ(scalar, -1); + + char buf[32]; + EXPECT_EQ(oaknode_keyframe_get_input(key, buf, sizeof(buf)), 9); + EXPECT_STREQ(buf, "float_in"); + + OakNodeNode *parent = nullptr; + EXPECT_EQ(oaknode_keyframe_get_parent(key, &parent), OAKNODE_OK); + EXPECT_EQ(parent, node_handle); + + oaknode_keyframe_free(key); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + + oaknode_keyframe_free(nullptr); // no crash +} + +TEST(NodeKeyframeTest, CreateErrorPaths) +{ + oaknode_value value = make_float(1.0); + // Invalid interpolation type. + EXPECT_EQ(oaknode_keyframe_create(0, 1, &value, 99, 0, -1, "x", nullptr), + nullptr); + // STRING does not fit the POD. + value.type = OAKNODE_VALUE_STRING; + EXPECT_EQ(oaknode_keyframe_create(0, 1, &value, OAKNODE_KEYFRAME_LINEAR, 0, + -1, "x", nullptr), + nullptr); +} + +TEST(NodeKeyframeTest, TimeLiveAndUndoable) +{ + OakNodeKeyframe *key = oaknode_keyframe_create(0, 1, nullptr, + OAKNODE_KEYFRAME_LINEAR, 0, + -1, "float_in", nullptr); + ASSERT_NE(key, nullptr); + + EXPECT_EQ(oaknode_keyframe_set_time(key, 1, 2), OAKNODE_OK); + int64_t num = 0, den = 0; + EXPECT_EQ(oaknode_keyframe_get_time(key, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 1); + EXPECT_EQ(den, 2); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_keyframe_set_time_undoable(key, 5, 1, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oaknode_keyframe_get_time(key, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 1); // not yet executed + + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_time(key, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 5); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_time(key, &num, &den), OAKNODE_OK); + EXPECT_EQ(num, 1); + + oakundo_command_free(command); + oaknode_keyframe_free(key); + + EXPECT_EQ(oaknode_keyframe_set_time(nullptr, 0, 1), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_keyframe_get_time(nullptr, &num, &den), + OAKNODE_E_INVALID); +} + +TEST(NodeKeyframeTest, ValueLiveAndUndoable) +{ + OakNodeKeyframe *key = oaknode_keyframe_create(0, 1, nullptr, + OAKNODE_KEYFRAME_LINEAR, 0, + -1, "float_in", nullptr); + ASSERT_NE(key, nullptr); + + oaknode_value in = make_float(3.25); + EXPECT_EQ(oaknode_keyframe_set_value(key, &in), OAKNODE_OK); + + // Orphan fallback: numeric values report as FLOAT. + oaknode_value out; + EXPECT_EQ(oaknode_keyframe_get_value(key, &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_FLOAT); + EXPECT_DOUBLE_EQ(out.f[0], 3.25); + + in = make_float(7.0); + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_keyframe_set_value_undoable(key, &in, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_value(key, &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 7.0); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_value(key, &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 3.25); + oakundo_command_free(command); + + // STRING rejected by the POD setter. + in.type = OAKNODE_VALUE_STRING; + EXPECT_EQ(oaknode_keyframe_set_value(key, &in), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_keyframe_set_value(key, nullptr), OAKNODE_E_INVALID); + + oaknode_keyframe_free(key); +} + +TEST(NodeKeyframeTest, StringValueLiveAndUndoable) +{ + OakNodeKeyframe *key = oaknode_keyframe_create(0, 1, nullptr, + OAKNODE_KEYFRAME_HOLD, 0, -1, + "text_in", nullptr); + ASSERT_NE(key, nullptr); + + EXPECT_EQ(oaknode_keyframe_set_value_string(key, "hello"), OAKNODE_OK); + char buf[16]; + EXPECT_EQ(oaknode_keyframe_get_value_string(key, buf, sizeof(buf)), 6); + EXPECT_STREQ(buf, "hello"); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_keyframe_set_value_string_undoable(key, "world", + &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_value_string(key, buf, sizeof(buf)), 6); + EXPECT_STREQ(buf, "world"); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_value_string(key, buf, sizeof(buf)), 6); + EXPECT_STREQ(buf, "hello"); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_keyframe_set_value_string(key, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_keyframe_get_value_string(nullptr, buf, sizeof(buf)), + OAKNODE_E_INVALID); + + oaknode_keyframe_free(key); +} + +TEST(NodeKeyframeTest, TypeLiveAndUndoable) +{ + OakNodeKeyframe *key = oaknode_keyframe_create(0, 1, nullptr, + OAKNODE_KEYFRAME_LINEAR, 0, + -1, "float_in", nullptr); + ASSERT_NE(key, nullptr); + + EXPECT_EQ(oaknode_keyframe_set_type(key, OAKNODE_KEYFRAME_HOLD), + OAKNODE_OK); + int type = -1; + EXPECT_EQ(oaknode_keyframe_get_type(key, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_KEYFRAME_HOLD); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_keyframe_set_type_undoable(key, OAKNODE_KEYFRAME_BEZIER, + &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_type(key, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_KEYFRAME_BEZIER); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_type(key, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_KEYFRAME_HOLD); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_keyframe_set_type(key, 99), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_keyframe_set_type(nullptr, OAKNODE_KEYFRAME_HOLD), + OAKNODE_E_INVALID); + + oaknode_keyframe_free(key); +} + +TEST(NodeKeyframeTest, BezierControlLiveAndUndoable) +{ + OakNodeKeyframe *key = oaknode_keyframe_create(0, 1, nullptr, + OAKNODE_KEYFRAME_BEZIER, 0, + -1, "float_in", nullptr); + ASSERT_NE(key, nullptr); + + EXPECT_EQ(oaknode_keyframe_set_bezier_control( + key, OAKNODE_KEYFRAME_IN_HANDLE, -1.0, 0.5), + OAKNODE_OK); + double x = 0.0, y = 0.0; + EXPECT_EQ(oaknode_keyframe_get_bezier_control( + key, OAKNODE_KEYFRAME_IN_HANDLE, &x, &y), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, -1.0); + EXPECT_DOUBLE_EQ(y, 0.5); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_keyframe_set_bezier_control_undoable( + key, OAKNODE_KEYFRAME_OUT_HANDLE, 2.0, -0.5, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_bezier_control( + key, OAKNODE_KEYFRAME_OUT_HANDLE, &x, &y), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, 2.0); + EXPECT_DOUBLE_EQ(y, -0.5); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_keyframe_get_bezier_control( + key, OAKNODE_KEYFRAME_OUT_HANDLE, &x, &y), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, 0.0); + EXPECT_DOUBLE_EQ(y, 0.0); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_keyframe_set_bezier_control(key, 99, 0.0, 0.0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_keyframe_get_bezier_control(nullptr, + OAKNODE_KEYFRAME_IN_HANDLE, + &x, &y), + OAKNODE_E_INVALID); + + oaknode_keyframe_free(key); +} + +} diff --git a/src/node/tests/node_test.cpp b/src/node/tests/node_test.cpp new file mode 100644 index 000000000..e43cdba78 --- /dev/null +++ b/src/node/tests/node_test.cpp @@ -0,0 +1,601 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/node.h" + +#include + +#include +#include + +#include "undo/undocommand.h" + +#include "../src/value.h" +#include "testnode.h" + +namespace +{ + +using oaknode_test::TestNode; +using oaknode_test::as_handle; + +std::string get_string(int (*fn)(const OakNodeNode *, char *, int), + const OakNodeNode *node) +{ + int required = fn(node, nullptr, 0); + EXPECT_GT(required, 0); + std::vector buf(static_cast(required)); + EXPECT_EQ(fn(node, buf.data(), required), required); + return std::string(buf.data()); +} + +oaknode_value make_float(double f) +{ + oaknode_value v = oaknode_value(); + v.type = OAKNODE_VALUE_FLOAT; + v.f[0] = f; + return v; +} + +TEST(NodeValueMappingTest, OakEnumOrdinalsArePinned) +{ + EXPECT_EQ(OAKNODE_VALUE_NONE, 0); + EXPECT_EQ(OAKNODE_VALUE_INT, 1); + EXPECT_EQ(OAKNODE_VALUE_FLOAT, 2); + EXPECT_EQ(OAKNODE_VALUE_BOOL, 3); + EXPECT_EQ(OAKNODE_VALUE_RATIONAL, 4); + EXPECT_EQ(OAKNODE_VALUE_COLOR, 5); + EXPECT_EQ(OAKNODE_VALUE_VEC2, 6); + EXPECT_EQ(OAKNODE_VALUE_VEC3, 7); + EXPECT_EQ(OAKNODE_VALUE_VEC4, 8); + EXPECT_EQ(OAKNODE_VALUE_COMBO, 9); + EXPECT_EQ(OAKNODE_VALUE_STRING, 10); +} + +TEST(NodeValueMappingTest, OliveToOakMappingIsPinned) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + struct { + const char *input; + int expected; + } cases[] = { + { "float_in", OAKNODE_VALUE_FLOAT }, { "int_in", OAKNODE_VALUE_INT }, + { "text_in", OAKNODE_VALUE_STRING }, { "color_in", OAKNODE_VALUE_COLOR }, + { "vec2_in", OAKNODE_VALUE_VEC2 }, { "rational_in", OAKNODE_VALUE_RATIONAL }, + { "enabled_in", OAKNODE_VALUE_BOOL }, + }; + + for (const auto &c : cases) { + int type = -1; + EXPECT_EQ(oaknode_node_input_get_type(handle, c.input, &type), + OAKNODE_OK); + EXPECT_EQ(type, c.expected) << c.input; + } +} + +TEST(NodeMetadataTest, IdNameLabelRoundtrip) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + EXPECT_EQ(get_string(oaknode_node_get_id, handle), TestNode::k_id); + EXPECT_EQ(get_string(oaknode_node_get_name, handle), "Test Node"); + EXPECT_EQ(get_string(oaknode_node_get_label, handle), ""); + + EXPECT_EQ(oaknode_node_set_label(handle, "hero"), OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_node_get_label, handle), "hero"); + + // Truncation still reports the full required size. + char small[3]; + EXPECT_EQ(oaknode_node_get_label(handle, small, sizeof(small)), 5); + EXPECT_STREQ(small, "he"); +} + +TEST(NodeMetadataTest, LabelUndoableSymmetry) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + EXPECT_EQ(oaknode_node_set_label(handle, "before"), OAKNODE_OK); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_label_undoable(handle, "after", &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(get_string(oaknode_node_get_label, handle), "before"); + + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(get_string(oaknode_node_get_label, handle), "after"); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(get_string(oaknode_node_get_label, handle), "before"); + + oakundo_command_free(command); +} + +TEST(NodeMetadataTest, OverrideColorLiveAndUndoable) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + int color = -2; + EXPECT_EQ(oaknode_node_get_override_color(handle, &color), OAKNODE_OK); + EXPECT_EQ(color, -1); + + EXPECT_EQ(oaknode_node_set_override_color(handle, 3), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_override_color(handle, &color), OAKNODE_OK); + EXPECT_EQ(color, 3); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_override_color_undoable(handle, 7, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_override_color(handle, &color), OAKNODE_OK); + EXPECT_EQ(color, 7); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_override_color(handle, &color), OAKNODE_OK); + EXPECT_EQ(color, 3); + oakundo_command_free(command); +} + +TEST(NodeMetadataTest, EnabledLiveAndUndoable) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + int enabled = 0; + EXPECT_EQ(oaknode_node_is_enabled(handle, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 1); + + EXPECT_EQ(oaknode_node_set_enabled(handle, 0), OAKNODE_OK); + EXPECT_EQ(oaknode_node_is_enabled(handle, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 0); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_enabled_undoable(handle, 1, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_is_enabled(handle, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 1); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_is_enabled(handle, &enabled), OAKNODE_OK); + EXPECT_EQ(enabled, 0); + oakundo_command_free(command); +} + +TEST(NodeInputTest, EnumerateInputs) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + int count = 0; + EXPECT_EQ(oaknode_node_input_count(handle, &count), OAKNODE_OK); + EXPECT_EQ(count, 7); // 6 TestNode inputs + enabled_in + + char buf[64]; + EXPECT_EQ(oaknode_node_input_id(handle, 0, buf, sizeof(buf)), 11); + EXPECT_STREQ(buf, "enabled_in"); + EXPECT_EQ(oaknode_node_input_id(handle, count, buf, sizeof(buf)), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_node_input_id(handle, -1, buf, sizeof(buf)), + OAKNODE_E_NOT_FOUND); + + int flags = 0; + EXPECT_EQ(oaknode_node_input_is_connectable(handle, "float_in", &flags), + OAKNODE_OK); + EXPECT_EQ(flags, 1); + EXPECT_EQ(oaknode_node_input_is_connectable(handle, "nope", &flags), + OAKNODE_E_NOT_FOUND); + + node.retranslate(); + EXPECT_EQ(oaknode_node_get_input_name(handle, "enabled_in", buf, + sizeof(buf)), + 8); + EXPECT_STREQ(buf, "Enabled"); +} + +TEST(NodeInputTest, ValueRoundtrip) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + oaknode_value out; + EXPECT_EQ(oaknode_node_get_input(handle, "float_in", &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_FLOAT); + EXPECT_DOUBLE_EQ(out.f[0], 0.0); + + oaknode_value in = make_float(2.5); + EXPECT_EQ(oaknode_node_set_input(handle, "float_in", &in), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "float_in", &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 2.5); + + in = oaknode_value(); + in.type = OAKNODE_VALUE_INT; + in.num = 42; + EXPECT_EQ(oaknode_node_set_input(handle, "int_in", &in), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "int_in", &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_INT); + EXPECT_EQ(out.num, 42); + + in = oaknode_value(); + in.type = OAKNODE_VALUE_RATIONAL; + in.num = 30000; + in.den = 1001; + EXPECT_EQ(oaknode_node_set_input(handle, "rational_in", &in), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "rational_in", &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_RATIONAL); + EXPECT_EQ(out.num, 30000); + EXPECT_EQ(out.den, 1001); + + in = oaknode_value(); + in.type = OAKNODE_VALUE_VEC2; + in.f[0] = 1.0; + in.f[1] = -2.0; + EXPECT_EQ(oaknode_node_set_input(handle, "vec2_in", &in), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "vec2_in", &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_VEC2); + EXPECT_DOUBLE_EQ(out.f[0], 1.0); + EXPECT_DOUBLE_EQ(out.f[1], -2.0); + + in = oaknode_value(); + in.type = OAKNODE_VALUE_COLOR; + in.f[0] = 0.1; + in.f[1] = 0.2; + in.f[2] = 0.3; + in.f[3] = 0.4; + EXPECT_EQ(oaknode_node_set_input(handle, "color_in", &in), OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "color_in", &out), OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_COLOR); + EXPECT_FLOAT_EQ(float(out.f[0]), 0.1f); + EXPECT_FLOAT_EQ(float(out.f[3]), 0.4f); +} + +TEST(NodeInputTest, ValueErrorPaths) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + oaknode_value out; + EXPECT_EQ(oaknode_node_get_input(nullptr, "float_in", &out), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_get_input(handle, "unknown_in", &out), + OAKNODE_E_NOT_FOUND); + // String-family input via the POD getter. + EXPECT_EQ(oaknode_node_get_input(handle, "text_in", &out), + OAKNODE_E_INVALID); + + // Type mismatch on set. + oaknode_value in = make_float(1.0); + EXPECT_EQ(oaknode_node_set_input(handle, "int_in", &in), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_set_input(handle, "unknown_in", &in), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_node_set_input(nullptr, "int_in", &in), + OAKNODE_E_INVALID); + in.type = OAKNODE_VALUE_STRING; + EXPECT_EQ(oaknode_node_set_input(handle, "text_in", &in), + OAKNODE_E_INVALID); +} + +TEST(NodeInputTest, ValueUndoableSymmetry) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + oaknode_value in = make_float(9.0); + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_input_undoable(handle, "float_in", &in, &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + + oaknode_value out; + EXPECT_EQ(oaknode_node_get_input(handle, "float_in", &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 0.0); // not yet executed + + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "float_in", &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 9.0); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_input(handle, "float_in", &out), OAKNODE_OK); + EXPECT_DOUBLE_EQ(out.f[0], 0.0); + oakundo_command_free(command); +} + +TEST(NodeInputTest, StringValueLiveAndUndoable) +{ + TestNode node; + OakNodeNode *handle = as_handle(&node); + + char buf[64]; + EXPECT_EQ(oaknode_node_get_input_string(handle, "text_in", buf, + sizeof(buf)), + 1); + EXPECT_EQ(oaknode_node_set_input_string(handle, "text_in", "hello"), + OAKNODE_OK); + int required = oaknode_node_get_input_string(handle, "text_in", buf, + sizeof(buf)); + EXPECT_EQ(required, 6); + EXPECT_STREQ(buf, "hello"); + + // Wrong family / unknown input. + EXPECT_EQ(oaknode_node_get_input_string(handle, "float_in", buf, + sizeof(buf)), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_set_input_string(handle, "unknown_in", "x"), + OAKNODE_E_NOT_FOUND); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_input_string_undoable(handle, "text_in", + "world", &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_input_string(handle, "text_in", buf, + sizeof(buf)), + 6); + EXPECT_STREQ(buf, "world"); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_input_string(handle, "text_in", buf, + sizeof(buf)), + 6); + EXPECT_STREQ(buf, "hello"); + oakundo_command_free(command); +} + +TEST(NodeGraphTest, ConnectDisconnectLive) +{ + TestNode source; + TestNode dest; + OakNodeNode *src = as_handle(&source); + OakNodeNode *dst = as_handle(&dest); + + int connected = -1; + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 0); + + EXPECT_EQ(oaknode_node_connect(src, dst, "float_in"), OAKNODE_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 1); + + OakNodeNode *upstream = nullptr; + EXPECT_EQ(oaknode_node_input_get_connected_node(dst, "float_in", &upstream), + OAKNODE_OK); + EXPECT_EQ(upstream, src); + + // Output side enumeration. + int count = 0; + EXPECT_EQ(oaknode_node_output_connection_count(src, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + OakNodeNode *downstream = nullptr; + EXPECT_EQ(oaknode_node_output_connection_node_at(src, 0, &downstream), + OAKNODE_OK); + EXPECT_EQ(downstream, dst); + char buf[64]; + EXPECT_EQ(oaknode_node_output_connection_input_id_at(src, 0, buf, + sizeof(buf)), + 9); + EXPECT_STREQ(buf, "float_in"); + int element = -2; + EXPECT_EQ(oaknode_node_output_connection_element_at(src, 0, &element), + OAKNODE_OK); + EXPECT_EQ(element, -1); + EXPECT_EQ(oaknode_node_output_connection_node_at(src, 1, &downstream), + OAKNODE_E_NOT_FOUND); + + // Double connect is a state error. + EXPECT_EQ(oaknode_node_connect(src, dst, "float_in"), OAKNODE_E_STATE); + // Unknown input id. + EXPECT_EQ(oaknode_node_connect(src, dst, "unknown_in"), + OAKNODE_E_NOT_FOUND); + + EXPECT_EQ(oaknode_node_disconnect(dst, "float_in"), OAKNODE_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 0); + EXPECT_EQ(oaknode_node_disconnect(dst, "float_in"), OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_node_disconnect(nullptr, "float_in"), OAKNODE_E_INVALID); +} + +TEST(NodeGraphTest, ConnectDisconnectUndoable) +{ + TestNode source; + TestNode dest; + OakNodeNode *src = as_handle(&source); + OakNodeNode *dst = as_handle(&dest); + + OakUndoCommand *add = nullptr; + EXPECT_EQ(oaknode_node_connect_undoable(src, dst, "float_in", &add), + OAKNODE_OK); + ASSERT_NE(add, nullptr); + + int connected = 0; + EXPECT_EQ(oakundo_command_redo_now(add), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 1); + EXPECT_EQ(oakundo_command_undo_now(add), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 0); + + // Redo again so the remove command has an edge to work on. + EXPECT_EQ(oakundo_command_redo_now(add), OAKUNDO_OK); + + OakUndoCommand *remove = nullptr; + EXPECT_EQ(oaknode_node_disconnect_undoable(dst, "float_in", &remove), + OAKNODE_OK); + ASSERT_NE(remove, nullptr); + EXPECT_EQ(oakundo_command_redo_now(remove), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 0); + EXPECT_EQ(oakundo_command_undo_now(remove), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_input_is_connected(dst, "float_in", &connected), + OAKNODE_OK); + EXPECT_EQ(connected, 1); + + EXPECT_EQ(oaknode_node_disconnect_undoable(dst, "int_in", &remove), + OAKNODE_E_NOT_FOUND); + + oakundo_command_free(remove); + oakundo_command_free(add); +} + +TEST(NodeLinkTest, LinkUnlinkLiveAndUndoable) +{ + TestNode a_node; + TestNode b_node; + OakNodeNode *a = as_handle(&a_node); + OakNodeNode *b = as_handle(&b_node); + + int linked = -1; + EXPECT_EQ(oaknode_node_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 0); + + int done = 0; + EXPECT_EQ(oaknode_node_link(a, b, &done), OAKNODE_OK); + EXPECT_EQ(done, 1); + EXPECT_EQ(oaknode_node_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 1); + + int count = 0; + EXPECT_EQ(oaknode_node_link_count(a, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + OakNodeNode *other = nullptr; + EXPECT_EQ(oaknode_node_link_at(a, 0, &other), OAKNODE_OK); + EXPECT_EQ(other, b); + EXPECT_EQ(oaknode_node_link_at(a, 1, &other), OAKNODE_E_NOT_FOUND); + + EXPECT_EQ(oaknode_node_unlink(a, b, &done), OAKNODE_OK); + EXPECT_EQ(done, 1); + EXPECT_EQ(oaknode_node_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 0); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_link_undoable(a, b, 1, &command), OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 1); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_are_linked(a, b, &linked), OAKNODE_OK); + EXPECT_EQ(linked, 0); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_node_link(nullptr, b, &done), OAKNODE_E_INVALID); +} + +TEST(NodeContextTest, PositionsLiveAndUndoable) +{ + TestNode node; + TestNode context; + OakNodeNode *n = as_handle(&node); + OakNodeNode *ctx = as_handle(&context); + + double x = 0.0, y = 0.0; + int expanded = -1; + EXPECT_EQ(oaknode_node_get_context_position(n, ctx, &x, &y, &expanded), + OAKNODE_E_NOT_FOUND); + + EXPECT_EQ(oaknode_node_set_context_position(n, ctx, 10.5, -4.0, 1), + OAKNODE_OK); + EXPECT_EQ(oaknode_node_get_context_position(n, ctx, &x, &y, &expanded), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, 10.5); + EXPECT_DOUBLE_EQ(y, -4.0); + EXPECT_EQ(expanded, 1); + + int count = 0; + EXPECT_EQ(oaknode_node_context_count(n, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + OakNodeNode *entry = nullptr; + EXPECT_EQ(oaknode_node_context_node_at(n, 0, &entry), OAKNODE_OK); + EXPECT_EQ(entry, ctx); + EXPECT_EQ(oaknode_node_context_node_at(n, 1, &entry), OAKNODE_E_NOT_FOUND); + + OakUndoCommand *command = nullptr; + EXPECT_EQ(oaknode_node_set_context_position_undoable(n, ctx, 1.0, 2.0, 0, + &command), + OAKNODE_OK); + ASSERT_NE(command, nullptr); + EXPECT_EQ(oakundo_command_redo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_context_position(n, ctx, &x, &y, &expanded), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, 1.0); + EXPECT_DOUBLE_EQ(y, 2.0); + EXPECT_EQ(expanded, 0); + EXPECT_EQ(oakundo_command_undo_now(command), OAKUNDO_OK); + EXPECT_EQ(oaknode_node_get_context_position(n, ctx, &x, &y, &expanded), + OAKNODE_OK); + EXPECT_DOUBLE_EQ(x, 10.5); + oakundo_command_free(command); + + EXPECT_EQ(oaknode_node_remove_from_context(n, ctx), OAKNODE_OK); + EXPECT_EQ(oaknode_node_remove_from_context(n, ctx), OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_node_context_count(n, &count), OAKNODE_OK); + EXPECT_EQ(count, 0); +} + +TEST(NodeLifetimeTest, CopyAndFree) +{ + TestNode node; + oaknode_value in = make_float(5.0); + EXPECT_EQ(oaknode_node_set_input(as_handle(&node), "float_in", &in), + OAKNODE_OK); + + int alive_before = oaknode_debug_alive_count(); + + OakNodeNode *copy = oaknode_node_create_copy(as_handle(&node)); + ASSERT_NE(copy, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + // copy() clones the type, not the values. + EXPECT_EQ(get_string(oaknode_node_get_id, copy), TestNode::k_id); + + oaknode_node_free(copy); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + + oaknode_node_free(nullptr); // no crash + EXPECT_EQ(oaknode_node_create_copy(nullptr), nullptr); +} + +TEST(NodeHandleTest, NullHandleErrors) +{ + char buf[16]; + int value = 0; + EXPECT_EQ(oaknode_node_get_id(nullptr, buf, sizeof(buf)), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_set_label(nullptr, "x"), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_get_override_color(nullptr, &value), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_is_enabled(nullptr, &value), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_input_count(nullptr, &value), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_are_linked(nullptr, nullptr, &value), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_context_count(nullptr, &value), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_node_output_connection_count(nullptr, &value), + OAKNODE_E_INVALID); +} + +} diff --git a/src/node/tests/project_test.cpp b/src/node/tests/project_test.cpp new file mode 100644 index 000000000..905f6d422 --- /dev/null +++ b/src/node/tests/project_test.cpp @@ -0,0 +1,266 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "node/folder.h" +#include "node/project.h" + +namespace +{ + +/** + * @brief Two-stage string getter helper: query, then fetch. + */ +std::string get_string(int (*fn)(const OakNodeProject *, char *, int), + const OakNodeProject *project) +{ + int required = fn(project, nullptr, 0); + if (required <= 0) { + return std::string(); + } + std::vector buf(static_cast(required)); + EXPECT_EQ(fn(project, buf.data(), required), required); + return std::string(buf.data()); +} + +} // namespace + +TEST(NodeProject, InitAndFree) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + // No root folder before initialize() + EXPECT_EQ(oaknode_project_root(project), nullptr); + + EXPECT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + EXPECT_NE(oaknode_project_root(project), nullptr); + + // Initializing twice is a state error + EXPECT_EQ(oaknode_project_initialize(project), OAKNODE_E_STATE); + + oaknode_project_free(project); + + // NULL free is a no-op (must not crash) + oaknode_project_free(nullptr); +} + +TEST(NodeProject, NullHandleErrors) +{ + EXPECT_EQ(oaknode_project_initialize(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_clear(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_root(nullptr), nullptr); + EXPECT_EQ(oaknode_project_name(nullptr, nullptr, 0), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_filename(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_pretty_filename(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_set_filename(nullptr, "/tmp/x.ove"), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_is_modified(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_set_modified(nullptr, 1), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_is_new(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_cache_path(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_get_cache_location_setting(nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_set_cache_location_setting(nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_get_custom_cache_path(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_set_custom_cache_path(nullptr, "/tmp"), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_get_uuid(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_node_count(nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_node_at(nullptr, 0), nullptr); +} + +TEST(NodeProject, NameAndFilename) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + // Untitled project + EXPECT_EQ(get_string(oaknode_project_name, project), "(untitled)"); + EXPECT_EQ(get_string(oaknode_project_filename, project), ""); + EXPECT_EQ(get_string(oaknode_project_pretty_filename, project), + "(untitled)"); + + EXPECT_EQ(oaknode_project_set_filename(project, "/tmp/work/my edit.ove"), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_project_name, project), "my edit"); + EXPECT_EQ(get_string(oaknode_project_filename, project), + "/tmp/work/my edit.ove"); + EXPECT_EQ(get_string(oaknode_project_pretty_filename, project), + "/tmp/work/my edit.ove"); + + // NULL filename argument is invalid + EXPECT_EQ(oaknode_project_set_filename(project, nullptr), + OAKNODE_E_INVALID); + + oaknode_project_free(project); +} + +TEST(NodeProject, ModifiedAndIsNew) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + EXPECT_EQ(oaknode_project_is_modified(project), 0); + EXPECT_EQ(oaknode_project_is_new(project), 1); + + EXPECT_EQ(oaknode_project_set_modified(project, 1), OAKNODE_OK); + EXPECT_EQ(oaknode_project_is_modified(project), 1); + EXPECT_EQ(oaknode_project_is_new(project), 0); + + EXPECT_EQ(oaknode_project_set_modified(project, 0), OAKNODE_OK); + EXPECT_EQ(oaknode_project_is_modified(project), 0); + + // A filename makes the project not-new even when unmodified + EXPECT_EQ(oaknode_project_set_filename(project, "/tmp/a.ove"), + OAKNODE_OK); + EXPECT_EQ(oaknode_project_is_new(project), 0); + + oaknode_project_free(project); +} + +TEST(NodeProject, CachePathSettings) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + // Default location setting + EXPECT_EQ(oaknode_project_get_cache_location_setting(project), 0); + EXPECT_EQ(get_string(oaknode_project_get_custom_cache_path, project), ""); + + // Invalid setting values are rejected + EXPECT_EQ(oaknode_project_set_cache_location_setting(project, -1), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_set_cache_location_setting(project, 3), + OAKNODE_E_INVALID); + + // Custom path setting is honored by cache_path() + EXPECT_EQ(oaknode_project_set_custom_cache_path(project, "/tmp/oakcache"), + OAKNODE_OK); + EXPECT_EQ(oaknode_project_set_cache_location_setting(project, 2), + OAKNODE_OK); + EXPECT_EQ(oaknode_project_get_cache_location_setting(project), 2); + EXPECT_EQ(get_string(oaknode_project_cache_path, project), + "/tmp/oakcache"); + EXPECT_EQ(get_string(oaknode_project_get_custom_cache_path, project), + "/tmp/oakcache"); + + // NULL clears the custom path + EXPECT_EQ(oaknode_project_set_custom_cache_path(project, nullptr), + OAKNODE_OK); + EXPECT_EQ(get_string(oaknode_project_get_custom_cache_path, project), ""); + + oaknode_project_free(project); +} + +TEST(NodeProject, Uuid) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + std::string uuid = get_string(oaknode_project_get_uuid, project); + EXPECT_FALSE(uuid.empty()); + + oaknode_project_free(project); +} + +TEST(NodeProject, AddRemoveNode) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + + // The root folder is the only node after initialize() + int base_count = oaknode_project_node_count(project); + EXPECT_EQ(base_count, 1); + + // folder_create already adds the node to the graph + OakNodeFolder *folder = oaknode_folder_create(project); + ASSERT_NE(folder, nullptr); + EXPECT_EQ(oaknode_project_node_count(project), base_count + 1); + + // node_at returns the same node; out-of-range yields NULL + OakNodeNode *as_node = nullptr; + bool found = false; + for (int i = 0; i < oaknode_project_node_count(project); i++) { + OakNodeNode *n = oaknode_project_node_at(project, i); + ASSERT_NE(n, nullptr); + if (n == reinterpret_cast(folder)) { + found = true; + } + if (i == 0) { + as_node = n; + } + } + EXPECT_TRUE(found); + EXPECT_EQ(oaknode_project_node_at(project, -1), nullptr); + EXPECT_EQ(oaknode_project_node_at(project, + oaknode_project_node_count(project)), + nullptr); + + // Remove detaches without deleting + OakNodeNode *folder_node = reinterpret_cast(folder); + EXPECT_EQ(oaknode_project_remove_node(project, folder_node), OAKNODE_OK); + EXPECT_EQ(oaknode_project_node_count(project), base_count); + + // Removing a node that is not in the graph is E_NOT_FOUND + EXPECT_EQ(oaknode_project_remove_node(project, folder_node), + OAKNODE_E_NOT_FOUND); + + // Re-add takes ownership back so the project frees it + EXPECT_EQ(oaknode_project_add_node(project, folder_node), OAKNODE_OK); + EXPECT_EQ(oaknode_project_node_count(project), base_count + 1); + + // NULL args + EXPECT_EQ(oaknode_project_add_node(project, nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_project_remove_node(project, nullptr), + OAKNODE_E_INVALID); + + oaknode_project_free(project); +} + +TEST(NodeProject, Clear) +{ + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + ASSERT_NE(oaknode_folder_create(project), nullptr); + EXPECT_GE(oaknode_project_node_count(project), 2); + + EXPECT_EQ(oaknode_project_clear(project), OAKNODE_OK); + EXPECT_EQ(oaknode_project_node_count(project), 0); + + // clear() resets root_, so re-initializing must succeed + EXPECT_EQ(oaknode_project_initialize(project), OAKNODE_OK); + EXPECT_GE(oaknode_project_node_count(project), 1); + + oaknode_project_free(project); +} diff --git a/src/node/tests/sequence_test.cpp b/src/node/tests/sequence_test.cpp new file mode 100644 index 000000000..6065a9608 --- /dev/null +++ b/src/node/tests/sequence_test.cpp @@ -0,0 +1,317 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "node/block.h" +#include "node/node.h" +#include "node/error.h" +#include "node/sequence.h" +#include "node/track.h" + +namespace +{ + +constexpr uint64_t k_stereo_layout = 0x3; // AV_CH_LAYOUT_STEREO +constexpr int k_sample_format_f32_packed = 10; // SampleFormat::f32 + +void expect_rational(int num, int den, int expected_num, int expected_den) +{ + EXPECT_EQ(num, expected_num); + EXPECT_EQ(den, expected_den); +} + +} // namespace + +TEST(SequenceTest, CreateFree) +{ + int base = oaknode_debug_alive_count(); + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), base + 1); + oaknode_sequence_free(seq); + EXPECT_EQ(oaknode_debug_alive_count(), base); + oaknode_sequence_free(nullptr); +} + +TEST(SequenceTest, NullHandleReturnsInvalid) +{ + int v; + OakNodeTrackList *list; + EXPECT_EQ(oaknode_sequence_get_track_list(nullptr, OAKNODE_TRACK_TYPE_VIDEO, + &list), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_sequence_get_track_count(nullptr, 0, &v), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_sequence_get_playhead(nullptr, &v, &v), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_sequence_get_video_params(nullptr, 0, nullptr), + OAKNODE_E_INVALID); +} + +TEST(SequenceTest, TrackLists) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + for (int type = OAKNODE_TRACK_TYPE_VIDEO; type < OAKNODE_TRACK_TYPE_COUNT; + type++) { + OakNodeTrackList *list = nullptr; + ASSERT_EQ(oaknode_sequence_get_track_list(seq, type, &list), + OAKNODE_OK); + ASSERT_NE(list, nullptr); + + int list_type = -1; + ASSERT_EQ(oaknode_tracklist_get_type(list, &list_type), OAKNODE_OK); + EXPECT_EQ(list_type, type); + + int count = -1; + ASSERT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK); + EXPECT_EQ(count, 0); + } + + OakNodeTrackList *list; + EXPECT_EQ(oaknode_sequence_get_track_list(seq, OAKNODE_TRACK_TYPE_NONE, + &list), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_sequence_get_track_count(seq, OAKNODE_TRACK_TYPE_COUNT, + nullptr), + OAKNODE_E_INVALID); + + oaknode_sequence_free(seq); +} + +TEST(SequenceTest, AddAndRemoveTracks) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + OakNodeTrackList *video = nullptr; + ASSERT_EQ(oaknode_sequence_get_track_list(seq, OAKNODE_TRACK_TYPE_VIDEO, + &video), + OAKNODE_OK); + + OakNodeTrack *t1 = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + OakNodeTrack *t2 = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t1, nullptr); + ASSERT_NE(t2, nullptr); + + ASSERT_EQ(oaknode_tracklist_add_track(video, t1), OAKNODE_OK); + ASSERT_EQ(oaknode_tracklist_add_track(video, t2), OAKNODE_OK); + + int count = 0; + ASSERT_EQ(oaknode_sequence_get_track_count(seq, OAKNODE_TRACK_TYPE_VIDEO, + &count), + OAKNODE_OK); + EXPECT_EQ(count, 2); + + // Indexes were assigned in list order + int index = -1; + ASSERT_EQ(oaknode_track_get_index(t1, &index), OAKNODE_OK); + EXPECT_EQ(index, 0); + ASSERT_EQ(oaknode_track_get_index(t2, &index), OAKNODE_OK); + EXPECT_EQ(index, 1); + + // Second track inherits the first track's height + double h1 = 0.0, h2 = 0.0; + ASSERT_EQ(oaknode_track_get_height(t1, &h1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_height(t2, &h2), OAKNODE_OK); + EXPECT_DOUBLE_EQ(h1, h2); + + // Array size tracks the number of connected tracks + int array_size = 0; + ASSERT_EQ(oaknode_tracklist_get_array_size(video, &array_size), OAKNODE_OK); + EXPECT_EQ(array_size, 2); + + // Sequence back-pointer and reference + OakNodeSequence *owner = nullptr; + ASSERT_EQ(oaknode_track_get_sequence(t1, &owner), OAKNODE_OK); + EXPECT_EQ(owner, seq); + + // Flat cache spans all types + int all = 0; + ASSERT_EQ(oaknode_sequence_get_all_track_count(seq, &all), OAKNODE_OK); + EXPECT_EQ(all, 2); + OakNodeTrack *at = nullptr; + ASSERT_EQ(oaknode_sequence_get_all_track_at(seq, 1, &at), OAKNODE_OK); + EXPECT_EQ(at, t2); + EXPECT_EQ(oaknode_sequence_get_all_track_at(seq, 2, &at), + OAKNODE_E_NOT_FOUND); + + // Borrowed lookup by type/index + ASSERT_EQ(oaknode_sequence_get_track_at(seq, OAKNODE_TRACK_TYPE_VIDEO, 0, + &at), + OAKNODE_OK); + EXPECT_EQ(at, t1); + EXPECT_EQ(oaknode_sequence_get_track_at(seq, OAKNODE_TRACK_TYPE_AUDIO, 0, + &at), + OAKNODE_E_NOT_FOUND); + + // Remove the first track; the second shifts down + ASSERT_EQ(oaknode_tracklist_remove_track(video, t1), OAKNODE_OK); + ASSERT_EQ(oaknode_sequence_get_track_count(seq, OAKNODE_TRACK_TYPE_VIDEO, + &count), + OAKNODE_OK); + EXPECT_EQ(count, 1); + ASSERT_EQ(oaknode_sequence_get_track_at(seq, OAKNODE_TRACK_TYPE_VIDEO, 0, + &at), + OAKNODE_OK); + EXPECT_EQ(at, t2); + ASSERT_EQ(oaknode_track_get_index(t2, &index), OAKNODE_OK); + EXPECT_EQ(index, 0); + + // Removing a foreign track reports NOT_FOUND + EXPECT_EQ(oaknode_tracklist_remove_track(video, t1), OAKNODE_E_NOT_FOUND); + + ASSERT_EQ(oaknode_tracklist_remove_track(video, t2), OAKNODE_OK); + oaknode_track_free(t1); + oaknode_track_free(t2); + oaknode_sequence_free(seq); +} + +TEST(SequenceTest, TrackLengthFlowsIntoSequence) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + OakNodeTrackList *video = nullptr; + ASSERT_EQ(oaknode_sequence_get_track_list(seq, OAKNODE_TRACK_TYPE_VIDEO, + &video), + OAKNODE_OK); + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + ASSERT_EQ(oaknode_tracklist_add_track(video, t), OAKNODE_OK); + + OakNodeBlock *gap = oaknode_block_gap_create(); + ASSERT_NE(gap, nullptr); + ASSERT_EQ(oaknode_block_set_length_and_media_out(gap, 5, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_append_block(t, gap), OAKNODE_OK); + + int num, den; + ASSERT_EQ(oaknode_tracklist_get_total_length(video, &num, &den), + OAKNODE_OK); + expect_rational(num, den, 5, 1); + + ASSERT_EQ(oaknode_sequence_verify_length(seq), OAKNODE_OK); + ASSERT_EQ(oaknode_sequence_get_video_length(seq, &num, &den), OAKNODE_OK); + expect_rational(num, den, 5, 1); + ASSERT_EQ(oaknode_sequence_get_length(seq, &num, &den), OAKNODE_OK); + expect_rational(num, den, 5, 1); + + ASSERT_EQ(oaknode_track_ripple_remove_block(t, gap), OAKNODE_OK); + oaknode_block_free(gap); + ASSERT_EQ(oaknode_tracklist_remove_track(video, t), OAKNODE_OK); + oaknode_track_free(t); + oaknode_sequence_free(seq); +} + +TEST(SequenceTest, Playhead) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + int num = -1, den = -1; + ASSERT_EQ(oaknode_sequence_get_playhead(seq, &num, &den), OAKNODE_OK); + + ASSERT_EQ(oaknode_sequence_set_playhead(seq, 7, 2), OAKNODE_OK); + ASSERT_EQ(oaknode_sequence_get_playhead(seq, &num, &den), OAKNODE_OK); + expect_rational(num, den, 7, 2); + + oaknode_sequence_free(seq); +} + +TEST(SequenceTest, VideoParamsRoundTrip) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + int count = 0; + ASSERT_EQ(oaknode_sequence_get_video_stream_count(seq, &count), + OAKNODE_OK); + ASSERT_GE(count, 1); + + // Default slot is readable + OakCommonVideoParams *params = nullptr; + ASSERT_EQ(oaknode_sequence_get_video_params(seq, 0, ¶ms), OAKNODE_OK); + ASSERT_NE(params, nullptr); + oakcommon_videoparams_free(params); + + // Out of range + EXPECT_EQ(oaknode_sequence_get_video_params(seq, count, ¶ms), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_sequence_set_video_params(seq, count, nullptr), + OAKNODE_E_INVALID); + + // Replace with explicit 1920x1080 @ 25fps params + OakCommonVideoParams *replacement = oakcommon_videoparams_init_with_time_base( + 1920, 1080, 1, 25, 0 /*pixel_format*/, 4 /*nb_channels*/, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + ASSERT_NE(replacement, nullptr); + ASSERT_EQ(oaknode_sequence_set_video_params(seq, 0, replacement), + OAKNODE_OK); + oakcommon_videoparams_free(replacement); + + OakCommonVideoParams *readback = nullptr; + ASSERT_EQ(oaknode_sequence_get_video_params(seq, 0, &readback), OAKNODE_OK); + ASSERT_NE(readback, nullptr); + int width = 0, height = 0, tb_num = 0, tb_den = 0; + ASSERT_EQ(oakcommon_videoparams_get_width(readback, &width), OAKCOMMON_OK); + ASSERT_EQ(oakcommon_videoparams_get_height(readback, &height), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_videoparams_get_time_base(readback, &tb_num, &tb_den), + OAKCOMMON_OK); + EXPECT_EQ(width, 1920); + EXPECT_EQ(height, 1080); + expect_rational(tb_num, tb_den, 1, 25); + oakcommon_videoparams_free(readback); + + oaknode_sequence_free(seq); +} + +TEST(SequenceTest, AudioParamsRoundTrip) +{ + OakNodeSequence *seq = oaknode_sequence_create(); + ASSERT_NE(seq, nullptr); + + int count = 0; + ASSERT_EQ(oaknode_sequence_get_audio_stream_count(seq, &count), + OAKNODE_OK); + ASSERT_GE(count, 1); + + OakAudioParams *params = oakcore_audioparams_create( + 48000, k_stereo_layout, k_sample_format_f32_packed); + ASSERT_NE(params, nullptr); + ASSERT_EQ(oaknode_sequence_set_audio_params(seq, 0, params), OAKNODE_OK); + oakcore_audioparams_free(params); + + OakAudioParams *readback = nullptr; + ASSERT_EQ(oaknode_sequence_get_audio_params(seq, 0, &readback), OAKNODE_OK); + ASSERT_NE(readback, nullptr); + EXPECT_EQ(oakcore_audioparams_sample_rate(readback), 48000); + EXPECT_EQ(oakcore_audioparams_channel_layout(readback), k_stereo_layout); + EXPECT_EQ(oakcore_audioparams_channel_count(readback), 2); + oakcore_audioparams_free(readback); + + EXPECT_EQ(oaknode_sequence_get_audio_params(seq, count, &readback), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_sequence_set_audio_params(seq, 0, nullptr), + OAKNODE_E_INVALID); + + oaknode_sequence_free(seq); +} diff --git a/src/node/tests/serializer_test.cpp b/src/node/tests/serializer_test.cpp new file mode 100644 index 000000000..6e2133bce --- /dev/null +++ b/src/node/tests/serializer_test.cpp @@ -0,0 +1,242 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include +#include + +#include "node/folder.h" +#include "node/serializer.h" + +namespace +{ + +/** + * @brief Serialize `save_data` to an XML string using the two-stage getter. + */ +std::string save_to_string(OakNodeSerializerSaveData *save_data) +{ + int required = oaknode_serializer_save_to_xml(save_data, nullptr, 0); + if (required <= 0) { + return std::string(); + } + std::vector buf(static_cast(required)); + EXPECT_EQ(oaknode_serializer_save_to_xml(save_data, buf.data(), required), + required); + return std::string(buf.data()); +} + +} // namespace + +// NOTE: this test must stay first in the file; it exercises the +// not-initialized state and leaves the serializers initialized behind it. +TEST(NodeSerializer, SaveWithoutInitializeFails) +{ + oaknode_serializer_shutdown(); + + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + OakNodeSerializerSaveData *save_data = oaknode_serializer_savedata_create( + OAKNODE_SERIALIZER_LOAD_ONLY_NODES, project); + ASSERT_NE(save_data, nullptr); + EXPECT_EQ(oaknode_serializer_save_to_xml(save_data, nullptr, 0), + OAKNODE_E_STATE); + + int result = -1; + EXPECT_EQ(oaknode_serializer_load_from_xml(project, "", + OAKNODE_SERIALIZER_LOAD_ONLY_NODES, + &result, nullptr, nullptr, 0), + OAKNODE_E_STATE); + + oaknode_serializer_savedata_free(save_data); + oaknode_project_free(project); +} + +TEST(NodeSerializer, InitializeIsIdempotent) +{ + EXPECT_EQ(oaknode_serializer_initialize(), OAKNODE_OK); + EXPECT_EQ(oaknode_serializer_initialize(), OAKNODE_OK); +} + +TEST(NodeSerializer, NodeGraphCopyPasteRoundTrip) +{ + ASSERT_EQ(oaknode_serializer_initialize(), OAKNODE_OK); + + // Source project with a folder under the root + OakNodeProject *source = oaknode_project_init(); + ASSERT_NE(source, nullptr); + ASSERT_EQ(oaknode_project_initialize(source), OAKNODE_OK); + OakNodeFolder *root = oaknode_project_root(source); + OakNodeFolder *folder = oaknode_folder_create(source); + ASSERT_NE(root, nullptr); + ASSERT_NE(folder, nullptr); + ASSERT_EQ(oaknode_folder_add_child( + root, reinterpret_cast(folder)), + OAKNODE_OK); + + // "Copy": serialize the folder with a custom property + OakNodeSerializerSaveData *save_data = oaknode_serializer_savedata_create( + OAKNODE_SERIALIZER_LOAD_ONLY_NODES, source); + ASSERT_NE(save_data, nullptr); + OakNodeNode *nodes[] = { reinterpret_cast(folder) }; + EXPECT_EQ(oaknode_serializer_savedata_set_nodes(save_data, nodes, 1), + OAKNODE_OK); + EXPECT_EQ(oaknode_serializer_savedata_set_property( + save_data, nodes[0], "nodeviewpos", "10;20"), + OAKNODE_OK); + + std::string xml = save_to_string(save_data); + EXPECT_FALSE(xml.empty()); + EXPECT_NE(xml.find(" prop_buf(static_cast(required)); + EXPECT_EQ(oaknode_serializer_loaddata_get_property(load_data, loaded, + "nodeviewpos", + prop_buf.data(), + required), + required); + EXPECT_STREQ(prop_buf.data(), "10;20"); + + // Unknown keys / nodes are E_NOT_FOUND + EXPECT_EQ(oaknode_serializer_loaddata_get_property(load_data, loaded, + "nosuchkey", nullptr, + 0), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_serializer_loaddata_get_property( + load_data, nodes[0], "nodeviewpos", nullptr, 0), + OAKNODE_E_NOT_FOUND); + + // Adopt the loaded nodes into the target project so it owns them + for (int i = 0; i < oaknode_serializer_loaddata_node_count(load_data); + i++) { + EXPECT_EQ(oaknode_project_add_node( + target, oaknode_serializer_loaddata_node_at(load_data, i)), + OAKNODE_OK); + } + + oaknode_serializer_loaddata_free(load_data); + oaknode_serializer_loaddata_free(nullptr); // NULL free is a no-op + // Detach the source hierarchy before teardown (see the folder tests' + // detach_all note about Project::clear()). + EXPECT_EQ(oaknode_folder_remove_child( + root, reinterpret_cast(folder)), + OAKNODE_OK); + oaknode_project_free(target); + oaknode_project_free(source); +} + +TEST(NodeSerializer, LoadInvalidXmlReportsSerializerError) +{ + ASSERT_EQ(oaknode_serializer_initialize(), OAKNODE_OK); + + OakNodeProject *project = oaknode_project_init(); + ASSERT_NE(project, nullptr); + + int result = -1; + OakNodeSerializerLoadData *load_data = nullptr; + char details[256] = { 0 }; + EXPECT_EQ(oaknode_serializer_load_from_xml( + project, "this is not xml", + OAKNODE_SERIALIZER_LOAD_ONLY_NODES, &result, &load_data, + details, sizeof(details)), + OAKNODE_OK); + // Not an oak document: the format version cannot be determined + EXPECT_EQ(result, OAKNODE_SERIALIZER_UNKNOWN_VERSION); + EXPECT_EQ(load_data, nullptr); + + oaknode_project_free(project); +} + +TEST(NodeSerializer, NullAndInvalidArgs) +{ + ASSERT_EQ(oaknode_serializer_initialize(), OAKNODE_OK); + + int result = -1; + + EXPECT_EQ(oaknode_serializer_savedata_create(-1, nullptr), nullptr); + EXPECT_EQ(oaknode_serializer_savedata_create(99, nullptr), nullptr); + + EXPECT_EQ(oaknode_serializer_savedata_set_nodes(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_savedata_set_property(nullptr, nullptr, + nullptr, nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_save_to_xml(nullptr, nullptr, 0), + OAKNODE_E_INVALID); + + EXPECT_EQ(oaknode_serializer_load_from_xml(nullptr, nullptr, 1, &result, + nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_load_from_xml(nullptr, "", 99, + &result, nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_load_from_xml(nullptr, "", 1, + nullptr, nullptr, nullptr, 0), + OAKNODE_E_INVALID); + + EXPECT_EQ(oaknode_serializer_loaddata_node_count(nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_loaddata_node_at(nullptr, 0), nullptr); + EXPECT_EQ(oaknode_serializer_loaddata_get_property(nullptr, nullptr, + nullptr, nullptr, 0), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_loaddata_connection_count(nullptr), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_serializer_loaddata_connection_at( + nullptr, 0, nullptr, nullptr, nullptr, 0, nullptr), + OAKNODE_E_INVALID); +} + +TEST(NodeSerializer, Shutdown) +{ + // Keep the global state clean for any families sharing this binary. + oaknode_serializer_shutdown(); +} diff --git a/src/node/tests/testnode.h b/src/node/tests/testnode.h new file mode 100644 index 000000000..550fbc58f --- /dev/null +++ b/src/node/tests/testnode.h @@ -0,0 +1,83 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_NODE_TESTS_TESTNODE_H +#define OAK_NODE_TESTS_TESTNODE_H + +// Shared test fixture node: a minimal concrete olive::Node subclass with +// one input per POD-carrying value type, so the C API tests can exercise +// value mapping without depending on NodeFactory. + +#include "../src/node.h" + +namespace oaknode_test +{ + +class TestNode : public olive::Node { +public: + static const char *k_id; + + TestNode() + { + add_input("float_in", olive::NodeValue::k_float, 0.0); + add_input("int_in", olive::NodeValue::k_int, int64_t(0)); + add_input("text_in", olive::NodeValue::k_text, std::string()); + add_input("color_in", olive::NodeValue::k_color, olive::core::Color()); + add_input("vec2_in", olive::NodeValue::k_vec2, olive::Vector2D()); + add_input("rational_in", olive::NodeValue::k_rational, + olive::Variant::from_value(olive::core::Rational(0, 1))); + } + + virtual ~TestNode() override + { + disconnect_all(); + } + + virtual olive::Node *copy() const override + { + return new TestNode(); + } + + virtual std::string name() const override + { + return "Test Node"; + } + + virtual std::string id() const override + { + return k_id; + } + + virtual std::vector category() const override + { + return { k_category_filter }; + } +}; + +inline const char *TestNode::k_id = "org.oak.TestNode"; + +inline OakNodeNode *as_handle(olive::Node *node) +{ + return reinterpret_cast(node); +} + +} + +#endif // OAK_NODE_TESTS_TESTNODE_H diff --git a/src/node/tests/track_test.cpp b/src/node/tests/track_test.cpp new file mode 100644 index 000000000..2a304b206 --- /dev/null +++ b/src/node/tests/track_test.cpp @@ -0,0 +1,339 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include "node/block.h" +#include "node/error.h" +#include "node/track.h" + +namespace +{ + +OakNodeBlock *make_gap(int length_num, int length_den) +{ + OakNodeBlock *gap = oaknode_block_gap_create(); + EXPECT_NE(gap, nullptr); + EXPECT_EQ(oaknode_block_set_length_and_media_out(gap, length_num, + length_den), + OAKNODE_OK); + return gap; +} + +void expect_rational(int num, int den, int expected_num, int expected_den) +{ + EXPECT_EQ(num, expected_num); + EXPECT_EQ(den, expected_den); +} + +} // namespace + +TEST(TrackTest, CreateFree) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + int type = -1; + ASSERT_EQ(oaknode_track_get_type(t, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_TRACK_TYPE_VIDEO); + oaknode_track_free(t); + + EXPECT_EQ(oaknode_track_create(OAKNODE_TRACK_TYPE_NONE), nullptr); + EXPECT_EQ(oaknode_track_create(OAKNODE_TRACK_TYPE_COUNT), nullptr); + oaknode_track_free(nullptr); +} + +TEST(TrackTest, NullHandleReturnsInvalid) +{ + int v; + double d; + EXPECT_EQ(oaknode_track_get_type(nullptr, &v), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_track_set_type(nullptr, OAKNODE_TRACK_TYPE_VIDEO), + OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_track_get_height(nullptr, &d), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_track_set_muted(nullptr, 1), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_track_append_block(nullptr, nullptr), OAKNODE_E_INVALID); + EXPECT_EQ(oaknode_tracklist_get_track_count(nullptr, &v), + OAKNODE_E_INVALID); +} + +TEST(TrackTest, TypeHeightIndexFlags) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_AUDIO); + ASSERT_NE(t, nullptr); + + ASSERT_EQ(oaknode_track_set_type(t, OAKNODE_TRACK_TYPE_SUBTITLE), + OAKNODE_OK); + int type = -1; + ASSERT_EQ(oaknode_track_get_type(t, &type), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_TRACK_TYPE_SUBTITLE); + EXPECT_EQ(oaknode_track_set_type(t, OAKNODE_TRACK_TYPE_NONE), + OAKNODE_E_INVALID); + + double h = 0.0; + ASSERT_EQ(oaknode_track_get_height(t, &h), OAKNODE_OK); + EXPECT_GT(h, 0.0); + ASSERT_EQ(oaknode_track_set_height(t, 4.5), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_height(t, &h), OAKNODE_OK); + EXPECT_DOUBLE_EQ(h, 4.5); + + int px = 0; + ASSERT_EQ(oaknode_track_set_height_in_pixels(t, 52), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_height_in_pixels(t, &px), OAKNODE_OK); + EXPECT_EQ(px, 52); + EXPECT_GT(oaknode_track_get_default_height_in_pixels(), 0); + EXPECT_GT(oaknode_track_get_minimum_height_in_pixels(), 0); + + int index = 99; + ASSERT_EQ(oaknode_track_get_index(t, &index), OAKNODE_OK); + EXPECT_EQ(index, -1); + ASSERT_EQ(oaknode_track_set_index(t, 3), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_index(t, &index), OAKNODE_OK); + EXPECT_EQ(index, 3); + + int flag = -1; + ASSERT_EQ(oaknode_track_get_muted(t, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 0); + ASSERT_EQ(oaknode_track_set_muted(t, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_muted(t, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 1); + + flag = -1; + ASSERT_EQ(oaknode_track_get_locked(t, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 0); + ASSERT_EQ(oaknode_track_set_locked(t, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_get_locked(t, &flag), OAKNODE_OK); + EXPECT_EQ(flag, 1); + + // Fresh track has no sequence + OakNodeSequence *seq = reinterpret_cast(0x1); + ASSERT_EQ(oaknode_track_get_sequence(t, &seq), OAKNODE_OK); + EXPECT_EQ(seq, nullptr); + + oaknode_track_free(t); +} + +TEST(TrackTest, AppendAndQueryBlocks) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + + int count = -1; + ASSERT_EQ(oaknode_track_get_block_count(t, &count), OAKNODE_OK); + EXPECT_EQ(count, 0); + + OakNodeBlock *a = make_gap(1, 1); + OakNodeBlock *b = make_gap(2, 1); + ASSERT_EQ(oaknode_track_append_block(t, a), OAKNODE_OK); + ASSERT_EQ(oaknode_track_append_block(t, b), OAKNODE_OK); + + ASSERT_EQ(oaknode_track_get_block_count(t, &count), OAKNODE_OK); + EXPECT_EQ(count, 2); + + // In/out points are recomputed from the chain + int num, den; + ASSERT_EQ(oaknode_block_get_in(a, &num, &den), OAKNODE_OK); + expect_rational(num, den, 0, 1); + ASSERT_EQ(oaknode_block_get_in(b, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 1); + ASSERT_EQ(oaknode_block_get_out(b, &num, &den), OAKNODE_OK); + expect_rational(num, den, 3, 1); + + // Adjacency and back-pointer + OakNodeBlock *nb = nullptr; + ASSERT_EQ(oaknode_block_get_next(a, &nb), OAKNODE_OK); + EXPECT_EQ(nb, b); + ASSERT_EQ(oaknode_block_get_previous(b, &nb), OAKNODE_OK); + EXPECT_EQ(nb, a); + OakNodeTrack *owner = nullptr; + ASSERT_EQ(oaknode_block_get_track(a, &owner), OAKNODE_OK); + EXPECT_EQ(owner, t); + + // Length of the whole track + ASSERT_EQ(oaknode_track_get_length(t, &num, &den), OAKNODE_OK); + expect_rational(num, den, 3, 1); + + // Index / lookup + int index = -1; + ASSERT_EQ(oaknode_track_get_block_index(t, b, &index), OAKNODE_OK); + EXPECT_EQ(index, 1); + + OakNodeBlock *at = nullptr; + ASSERT_EQ(oaknode_track_get_block_at(t, 0, &at), OAKNODE_OK); + EXPECT_EQ(at, a); + EXPECT_EQ(oaknode_track_get_block_at(t, 2, &at), OAKNODE_E_NOT_FOUND); + + ASSERT_EQ(oaknode_track_get_block_containing_time(t, 1, 2, &at), + OAKNODE_OK); + EXPECT_EQ(at, a); + ASSERT_EQ(oaknode_track_get_block_containing_time(t, 3, 2, &at), + OAKNODE_OK); + EXPECT_EQ(at, b); + // Boundary time is not "contained" + EXPECT_EQ(oaknode_track_get_block_containing_time(t, 1, 1, &at), + OAKNODE_E_NOT_FOUND); + // Past the end + EXPECT_EQ(oaknode_track_get_visible_block_at_time(t, 10, 1, &at), + OAKNODE_E_NOT_FOUND); + + int free_range = -1; + ASSERT_EQ(oaknode_track_is_range_free(t, 3, 1, 5, 1, &free_range), + OAKNODE_OK); + EXPECT_EQ(free_range, 1); + // A range holding only GapBlocks still counts as free + ASSERT_EQ(oaknode_track_is_range_free(t, 0, 1, 2, 1, &free_range), + OAKNODE_OK); + EXPECT_EQ(free_range, 1); + + // Tear down manually (no project graph owns these) + ASSERT_EQ(oaknode_track_ripple_remove_block(t, a), OAKNODE_OK); + ASSERT_EQ(oaknode_track_ripple_remove_block(t, b), OAKNODE_OK); + oaknode_block_free(a); + oaknode_block_free(b); + oaknode_track_free(t); +} + +TEST(TrackTest, PrependInsertAndRippleRemove) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + + OakNodeBlock *a = make_gap(1, 1); + OakNodeBlock *b = make_gap(1, 1); + OakNodeBlock *c = make_gap(1, 1); + OakNodeBlock *d = make_gap(1, 1); + + ASSERT_EQ(oaknode_track_append_block(t, c), OAKNODE_OK); + ASSERT_EQ(oaknode_track_prepend_block(t, a), OAKNODE_OK); + ASSERT_EQ(oaknode_track_insert_block_at_index(t, b, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_insert_block_after(t, d, c), OAKNODE_OK); + + int count = 0; + ASSERT_EQ(oaknode_track_get_block_count(t, &count), OAKNODE_OK); + EXPECT_EQ(count, 4); + + OakNodeBlock *at = nullptr; + ASSERT_EQ(oaknode_track_get_block_at(t, 0, &at), OAKNODE_OK); + EXPECT_EQ(at, a); + ASSERT_EQ(oaknode_track_get_block_at(t, 1, &at), OAKNODE_OK); + EXPECT_EQ(at, b); + ASSERT_EQ(oaknode_track_get_block_at(t, 3, &at), OAKNODE_OK); + EXPECT_EQ(at, d); + + // Ripple-removing b shifts c back to t=1 + ASSERT_EQ(oaknode_track_ripple_remove_block(t, b), OAKNODE_OK); + int num, den; + ASSERT_EQ(oaknode_block_get_in(c, &num, &den), OAKNODE_OK); + expect_rational(num, den, 1, 1); + ASSERT_EQ(oaknode_track_get_length(t, &num, &den), OAKNODE_OK); + expect_rational(num, den, 3, 1); + + ASSERT_EQ(oaknode_track_ripple_remove_block(t, a), OAKNODE_OK); + ASSERT_EQ(oaknode_track_ripple_remove_block(t, c), OAKNODE_OK); + ASSERT_EQ(oaknode_track_ripple_remove_block(t, d), OAKNODE_OK); + oaknode_block_free(a); + oaknode_block_free(b); + oaknode_block_free(c); + oaknode_block_free(d); + oaknode_track_free(t); +} + +TEST(TrackTest, InsertBefore) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + + OakNodeBlock *a = make_gap(1, 1); + OakNodeBlock *b = make_gap(1, 1); + ASSERT_EQ(oaknode_track_append_block(t, b), OAKNODE_OK); + ASSERT_EQ(oaknode_track_insert_block_before(t, a, b), OAKNODE_OK); + + OakNodeBlock *at = nullptr; + ASSERT_EQ(oaknode_track_get_block_at(t, 0, &at), OAKNODE_OK); + EXPECT_EQ(at, a); + + ASSERT_EQ(oaknode_track_ripple_remove_block(t, a), OAKNODE_OK); + ASSERT_EQ(oaknode_track_ripple_remove_block(t, b), OAKNODE_OK); + oaknode_block_free(a); + oaknode_block_free(b); + oaknode_track_free(t); +} + +TEST(TrackTest, ReplaceBlock) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + + OakNodeBlock *old_gap = make_gap(2, 1); + OakNodeBlock *new_gap = make_gap(2, 1); + ASSERT_EQ(oaknode_track_append_block(t, old_gap), OAKNODE_OK); + ASSERT_EQ(oaknode_track_replace_block(t, old_gap, new_gap), OAKNODE_OK); + + int count = 0; + ASSERT_EQ(oaknode_track_get_block_count(t, &count), OAKNODE_OK); + EXPECT_EQ(count, 1); + OakNodeBlock *at = nullptr; + ASSERT_EQ(oaknode_track_get_block_at(t, 0, &at), OAKNODE_OK); + EXPECT_EQ(at, new_gap); + int num, den; + ASSERT_EQ(oaknode_track_get_length(t, &num, &den), OAKNODE_OK); + expect_rational(num, den, 2, 1); + + ASSERT_EQ(oaknode_track_ripple_remove_block(t, new_gap), OAKNODE_OK); + oaknode_block_free(old_gap); + oaknode_block_free(new_gap); + oaknode_track_free(t); +} + +TEST(TrackTest, RangeOccupiedByClipIsNotFree) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO); + ASSERT_NE(t, nullptr); + + OakNodeBlock *clip = oaknode_block_clip_create(); + ASSERT_NE(clip, nullptr); + ASSERT_EQ(oaknode_block_set_length_and_media_out(clip, 2, 1), OAKNODE_OK); + ASSERT_EQ(oaknode_track_append_block(t, clip), OAKNODE_OK); + + int free_range = -1; + ASSERT_EQ(oaknode_track_is_range_free(t, 0, 1, 2, 1, &free_range), + OAKNODE_OK); + EXPECT_EQ(free_range, 0); + ASSERT_EQ(oaknode_track_is_range_free(t, 2, 1, 4, 1, &free_range), + OAKNODE_OK); + EXPECT_EQ(free_range, 1); + + ASSERT_EQ(oaknode_track_ripple_remove_block(t, clip), OAKNODE_OK); + oaknode_block_free(clip); + oaknode_track_free(t); +} + +TEST(TrackTest, Reference) +{ + OakNodeTrack *t = oaknode_track_create(OAKNODE_TRACK_TYPE_AUDIO); + ASSERT_NE(t, nullptr); + ASSERT_EQ(oaknode_track_set_index(t, 2), OAKNODE_OK); + + int type = -1, index = -1; + ASSERT_EQ(oaknode_track_get_reference(t, &type, &index), OAKNODE_OK); + EXPECT_EQ(type, OAKNODE_TRACK_TYPE_AUDIO); + EXPECT_EQ(index, 2); + + oaknode_track_free(t); +} diff --git a/src/node/tests/traverser_test.cpp b/src/node/tests/traverser_test.cpp new file mode 100644 index 000000000..250fcad8f --- /dev/null +++ b/src/node/tests/traverser_test.cpp @@ -0,0 +1,153 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "node/traverser.h" + +#include + +#include + +#include "testnode.h" + +namespace +{ + +using oaknode_test::TestNode; +using oaknode_test::as_handle; + +TEST(NodeTraverserTest, InitFree) +{ + int alive_before = oaknode_debug_alive_count(); + + OakNodeTraverser *traverser = oaknode_traverser_init(); + ASSERT_NE(traverser, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + oaknode_traverser_free(traverser); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + + oaknode_traverser_free(nullptr); // no crash +} + +TEST(NodeTraverserTest, GenerateDatabaseAndEnumerate) +{ + TestNode node; + oaknode_value in = oaknode_value(); + in.type = OAKNODE_VALUE_FLOAT; + in.f[0] = 4.5; + ASSERT_EQ(oaknode_node_set_input(as_handle(&node), "float_in", &in), + OAKNODE_OK); + + OakNodeTraverser *traverser = oaknode_traverser_init(); + ASSERT_NE(traverser, nullptr); + + int alive_before = oaknode_debug_alive_count(); + + OakNodeValueDatabase *db = nullptr; + EXPECT_EQ(oaknode_traverser_generate_database(traverser, as_handle(&node), + 0, 1, 1, 1, &db), + OAKNODE_OK); + ASSERT_NE(db, nullptr); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before + 1); + + int rows = 0; + EXPECT_EQ(oaknode_traverser_database_row_count(db, &rows), OAKNODE_OK); + EXPECT_GT(rows, 0); + + // Find the float_in row and read its value back. + char key[64]; + int found = 0; + for (int i = 0; i < rows; i++) { + ASSERT_GT(oaknode_traverser_database_row_key_at(db, i, key, + sizeof(key)), + 1); + if (std::strcmp(key, "float_in") == 0) { + found = 1; + + int values = 0; + EXPECT_EQ(oaknode_traverser_database_row_value_count(db, key, + &values), + OAKNODE_OK); + ASSERT_GE(values, 1); + + oaknode_value out; + EXPECT_EQ(oaknode_traverser_database_value_at(db, key, 0, &out), + OAKNODE_OK); + EXPECT_EQ(out.type, OAKNODE_VALUE_FLOAT); + EXPECT_DOUBLE_EQ(out.f[0], 4.5); + + char text[64]; + EXPECT_GT(oaknode_traverser_database_value_string_at( + db, key, 0, text, sizeof(text)), + 1); + + EXPECT_EQ(oaknode_traverser_database_value_at(db, key, values, + &out), + OAKNODE_E_NOT_FOUND); + } + } + EXPECT_EQ(found, 1); + + // Error paths. + EXPECT_EQ(oaknode_traverser_database_row_key_at(db, rows, key, + sizeof(key)), + OAKNODE_E_NOT_FOUND); + int values = 0; + EXPECT_EQ(oaknode_traverser_database_row_value_count(db, "nope", &values), + OAKNODE_E_NOT_FOUND); + oaknode_value out; + EXPECT_EQ(oaknode_traverser_database_value_at(db, "nope", 0, &out), + OAKNODE_E_NOT_FOUND); + EXPECT_EQ(oaknode_traverser_database_value_at(nullptr, "float_in", 0, + &out), + OAKNODE_E_INVALID); + + oaknode_traverser_database_free(db); + EXPECT_EQ(oaknode_debug_alive_count(), alive_before); + oaknode_traverser_database_free(nullptr); // no crash + + oaknode_traverser_free(traverser); +} + +TEST(NodeTraverserTest, InvalidArguments) +{ + OakNodeValueDatabase *db = nullptr; + EXPECT_EQ(oaknode_traverser_generate_database(nullptr, nullptr, 0, 1, 1, 1, + &db), + OAKNODE_E_INVALID); + + OakNodeTraverser *traverser = oaknode_traverser_init(); + ASSERT_NE(traverser, nullptr); + EXPECT_EQ(oaknode_traverser_generate_database(traverser, nullptr, 0, 1, 1, + 1, &db), + OAKNODE_E_INVALID); + + TestNode node; + EXPECT_EQ(oaknode_traverser_generate_database(traverser, as_handle(&node), + 0, 1, 1, 1, nullptr), + OAKNODE_E_INVALID); + + EXPECT_EQ(oaknode_traverser_database_row_count(nullptr, nullptr), + OAKNODE_E_INVALID); + + oaknode_traverser_free(traverser); +} + +} diff --git a/src/node/transition/audio/audioprocessor.h b/src/node/transition/audio/audioprocessor.h new file mode 100644 index 000000000..29281f0cb --- /dev/null +++ b/src/node/transition/audio/audioprocessor.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include "olive/core/render/audioparams.h" +namespace olive { using core::AudioParams; } +namespace olive { +class AudioProcessor { +public: + using Buffer = std::vector>; + bool open(const AudioParams &, const AudioParams &, double = 1.0) { return true; } + void close() {} + bool is_open() const { return false; } + int convert(float **, int, Buffer *) { return 0; } + void flush() {} +}; +} diff --git a/src/node/transition/audio/audiovisualwaveform.h b/src/node/transition/audio/audiovisualwaveform.h new file mode 100644 index 000000000..1ef9295bd --- /dev/null +++ b/src/node/transition/audio/audiovisualwaveform.h @@ -0,0 +1,2 @@ +#pragma once +namespace olive { class AudioVisualWaveform {}; } diff --git a/src/node/transition/codec/decoder.h b/src/node/transition/codec/decoder.h new file mode 100644 index 000000000..b91a30f19 --- /dev/null +++ b/src/node/transition/codec/decoder.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include +#include "loopmode.h" +#include "render/cancelatom.h" +#include "project/footage/footagedescription.h" +namespace olive { +class Decoder; +using DecoderPtr = std::shared_ptr; +class Decoder { +public: + virtual ~Decoder() = default; + static std::vector receive_list_of_all_decoders() { return {}; } + FootageDescription probe(const std::string &, CancelAtom *) { return FootageDescription(); } +}; +} diff --git a/src/node/transition/codec/encoder.h b/src/node/transition/codec/encoder.h new file mode 100644 index 000000000..d277cefc2 --- /dev/null +++ b/src/node/transition/codec/encoder.h @@ -0,0 +1,7 @@ +#pragma once +namespace olive { +class EncodingParams { +public: + EncodingParams() {} +}; +} diff --git a/src/node/transition/codec/frame.h b/src/node/transition/codec/frame.h new file mode 100644 index 000000000..074ee49df --- /dev/null +++ b/src/node/transition/codec/frame.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include "videoparams.h" +#include "olive/core/util/color.h" +namespace olive { +using core::Color; +class Frame; +using FramePtr = std::shared_ptr; +class Frame { +public: + bool is_allocated() const { return false; } + void allocate() {} + char *data() { return nullptr; } + int64_t allocated_size() const { return 0; } + int width() const { return 0; } + int height() const { return 0; } + int linesize_bytes() const { return 0; } + int linesize_pixels() const { return 0; } + void set_pixel(int, int, const Color &) {} + const VideoParams &video_params() const + { + static VideoParams p; + return p; + } + void set_video_params(const VideoParams &) {} +}; +} diff --git a/src/node/transition/codec/proxymanager.h b/src/node/transition/codec/proxymanager.h new file mode 100644 index 000000000..c4176c2e6 --- /dev/null +++ b/src/node/transition/codec/proxymanager.h @@ -0,0 +1,23 @@ +#pragma once +#include +namespace olive { +class ProxyManager { +public: + enum ProxyState { k_proxy_missing, k_proxy_generating, k_proxy_ready }; + struct ProxyParams { + int width = 0; + int height = 0; + int divider = 1; + int crf = 0; + std::string preset; + std::string extension; + bool include_audio = false; + }; + static ProxyManager *instance() { static ProxyManager p; return &p; } + static std::string proxy_state_to_string(ProxyState) { return std::string(); } + static ProxyState proxy_state_from_string(const std::string &) { return k_proxy_missing; } + static ProxyState get_proxy_state(const std::string &) { return k_proxy_missing; } + static bool proxy_filename_has_audio(const std::string &) { return false; } + static ProxyParams proxy_params_from_config() { return ProxyParams(); } +}; +} diff --git a/src/node/transition/common/cancelableobject.h b/src/node/transition/common/cancelableobject.h new file mode 100644 index 000000000..df0224d17 --- /dev/null +++ b/src/node/transition/common/cancelableobject.h @@ -0,0 +1,2 @@ +#pragma once +namespace olive { class CancelableObject {}; } diff --git a/src/node/transition/common/current.h b/src/node/transition/common/current.h new file mode 100644 index 000000000..cfb0d86d0 --- /dev/null +++ b/src/node/transition/common/current.h @@ -0,0 +1,7 @@ +#pragma once +// Transitional: engine/common/current.h was sunk to oakcommon (current.h, +// type-erased de-Qt Current). Forward so existing "common/current.h" +// includes keep working. M7/M9 retarget callers to the real boundary. +// Angle brackets: a quoted "current.h" here would resolve to this file +// itself (same directory). +#include diff --git a/src/node/transition/common/lerp.h b/src/node/transition/common/lerp.h new file mode 100644 index 000000000..d21bc0fd4 --- /dev/null +++ b/src/node/transition/common/lerp.h @@ -0,0 +1,4 @@ +#pragma once +namespace olive { +template T lerp(const T &a, const T &b, float t) { return a + (b - a) * t; } +} diff --git a/src/node/transition/common/util.h b/src/node/transition/common/util.h new file mode 100644 index 000000000..80a6bfbfb --- /dev/null +++ b/src/node/transition/common/util.h @@ -0,0 +1,4 @@ +#pragma once +namespace olive { +template inline T mid(T a, T b) { return (a + b) / 2; } +} diff --git a/src/node/transition/config/config.h b/src/node/transition/config/config.h new file mode 100644 index 000000000..599effc22 --- /dev/null +++ b/src/node/transition/config/config.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include "variant.h" +#ifndef QStringLiteral +#define QStringLiteral(x) x +#endif +namespace olive { +class ConfigValue : public olive::Variant { +public: + ConfigValue() = default; + bool toBool() const { return false; } + int toInt() const { return 0; } +}; +class Config { +public: + static Config ¤t() { static Config c; return c; } + ConfigValue operator[](const std::string &) const { return ConfigValue(); } +}; +} +#ifndef OAK_CONFIG +#define OAK_CONFIG(x) Config::current()[x] +#endif +#ifndef OAK_CONFIG_STR +#define OAK_CONFIG_STR(x) Config::current()[x] +#endif diff --git a/src/node/transition/coreengine.h b/src/node/transition/coreengine.h new file mode 100644 index 000000000..d6265d11e --- /dev/null +++ b/src/node/transition/coreengine.h @@ -0,0 +1,30 @@ +#pragma once +// Transitional stub for engine/coreengine.h (still Qt-based). Only the +// surface oaknode uses; clipboard access stays a no-op until M9 wires the +// real engine boundary. Clipboard strings are std::string (de-Qt'd at this +// boundary per the ADAPT(M9) notes in serializer.cpp). +#include +#include "olive/core/util/timecodefunctions.h" +#include "undostack.h" +namespace olive { +class EngineCore { +public: + static EngineCore *instance() + { + static EngineCore e; + return &e; + } + core::Timecode::Display get_timecode_display() const + { + return core::Timecode::k_timecode_seconds; + } + UndoStack *undo_stack() + { + return &undo_stack_; + } + static void copy_string_to_clipboard(const std::string &) {} + static std::string paste_string_from_clipboard() { return std::string(); } +private: + UndoStack undo_stack_; +}; +} diff --git a/src/node/transition/pluginSupport/olivehost.h b/src/node/transition/pluginSupport/olivehost.h new file mode 100644 index 000000000..566ec65e9 --- /dev/null +++ b/src/node/transition/pluginSupport/olivehost.h @@ -0,0 +1,12 @@ +#pragma once +// Transitional stub for engine/pluginSupport/olivehost.h (still Qt-based). +// Only the surface oaknode uses. M9 replaces this with the real plugin +// boundary. +#include +#include "ofxhImageEffectAPI.h" +#include "ofxhPluginCache.h" +namespace olive { +namespace plugin { +inline void load_plugins(const std::string &) {} +} +} diff --git a/src/node/transition/pluginSupport/oliveplugininstance.h b/src/node/transition/pluginSupport/oliveplugininstance.h new file mode 100644 index 000000000..60bbf2d7d --- /dev/null +++ b/src/node/transition/pluginSupport/oliveplugininstance.h @@ -0,0 +1,30 @@ +#pragma once +// Minimal syntax-check stub for engine/pluginSupport/oliveplugininstance.h +// (real header is Qt-based, handled in a later milestone). Not in repo. +#include + +#include "ofxhImageEffect.h" + +namespace olive +{ +namespace plugin +{ +class PluginNode; +} + +class OlivePluginInstance : public OFX::Host::ImageEffect::Instance { +public: + void setNode(std::shared_ptr node) + { + node_ = node; + } + std::shared_ptr node() const + { + return node_; + } + +private: + std::shared_ptr node_; +}; + +} diff --git a/src/node/transition/render/audioplaybackcache.h b/src/node/transition/render/audioplaybackcache.h new file mode 100644 index 000000000..2743a9bee --- /dev/null +++ b/src/node/transition/render/audioplaybackcache.h @@ -0,0 +1,14 @@ +#pragma once +// Syntax-check stub only (not in repo). +#include +#include "olive/core/util/timerange.h" +#include "olive/core/render/audioparams.h" +namespace olive { using core::AudioParams; } +#include "render/playbackcache.h" +namespace olive { +class Node; +class AudioPlaybackCache : public PlaybackCache { +public: + template explicit AudioPlaybackCache(T *) : PlaybackCache(this) {} +}; +} diff --git a/src/node/transition/render/audiowaveformcache.h b/src/node/transition/render/audiowaveformcache.h new file mode 100644 index 000000000..33d9fa62c --- /dev/null +++ b/src/node/transition/render/audiowaveformcache.h @@ -0,0 +1,14 @@ +#pragma once +// Syntax-check stub only (not in repo). +#include +#include +#include "olive/core/util/timerange.h" +#include "render/playbackcache.h" +namespace olive { +class Node; +class AudioWaveformCache : public PlaybackCache { +public: + template explicit AudioWaveformCache(T *) : PlaybackCache(this) {} + void set_saving_enabled(bool) {} +}; +} diff --git a/src/node/transition/render/cancelatom.h b/src/node/transition/render/cancelatom.h new file mode 100644 index 000000000..bfb592a31 --- /dev/null +++ b/src/node/transition/render/cancelatom.h @@ -0,0 +1,8 @@ +#pragma once +namespace olive { +class CancelAtom { +public: + bool is_cancelled() const { return false; } + bool heard_cancel() const { return false; } +}; +} diff --git a/src/node/transition/render/colorprocessor.h b/src/node/transition/render/colorprocessor.h new file mode 100644 index 000000000..0d8238a95 --- /dev/null +++ b/src/node/transition/render/colorprocessor.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include "ocioutils.h" +#include "colortransform.h" +namespace olive { +class ColorManager; +class ColorProcessor; +using ColorProcessorPtr = std::shared_ptr; +class ColorProcessor { +public: + enum Direction { k_normal, k_inverse }; + static ColorProcessorPtr create(ColorManager *, const std::string &, + const ColorTransform &, + Direction = k_normal) + { + return nullptr; + } + static ColorProcessorPtr create(ocio::ConstProcessorRcPtr) + { + return nullptr; + } +}; +} diff --git a/src/node/transition/render/diskmanager.h b/src/node/transition/render/diskmanager.h new file mode 100644 index 000000000..40c8ab52e --- /dev/null +++ b/src/node/transition/render/diskmanager.h @@ -0,0 +1,9 @@ +#pragma once +#include +namespace olive { +class DiskManager { +public: + static DiskManager *instance() { static DiskManager d; return &d; } + std::string get_default_cache_path() const { return std::string(); } +}; +} diff --git a/src/node/transition/render/framehashcache.h b/src/node/transition/render/framehashcache.h new file mode 100644 index 000000000..e036de065 --- /dev/null +++ b/src/node/transition/render/framehashcache.h @@ -0,0 +1,32 @@ +#pragma once +// Transitional stub for engine/render/framehashcache.h (still Qt-based). +// Only the surface oaknode uses. M7 replaces this with the real oakrender +// boundary. +#include +#include +#include "olive/core/util/rational.h" +#include "olive/core/util/timerange.h" +#include "render/playbackcache.h" +namespace olive { +class Node; +class FrameHashCache : public PlaybackCache { +public: + template explicit FrameHashCache(T *) : PlaybackCache(this) {} + const core::Rational &get_timebase() const { return timebase_; } + void set_timebase(const core::Rational &t) { timebase_ = t; } + std::mutex &mutex() { return mutex_; } + void load_state() {} + std::string get_valid_cache_filename(const core::Rational &) const + { + return std::string(); + } + +private: + core::Rational timebase_; + std::mutex mutex_; +}; +class ThumbnailCache : public FrameHashCache { +public: + template explicit ThumbnailCache(T *) : FrameHashCache(this) {} +}; +} diff --git a/src/node/transition/render/job/acceleratedjob.h b/src/node/transition/render/job/acceleratedjob.h new file mode 100644 index 000000000..1f8163f20 --- /dev/null +++ b/src/node/transition/render/job/acceleratedjob.h @@ -0,0 +1,23 @@ +#pragma once +#include "param.h" +#include "value.h" +namespace olive { +class AcceleratedJob { +public: + virtual ~AcceleratedJob() = default; + virtual void insert(const std::string &input, const NodeValueRow &row) + { + (void) input; (void) row; + } + virtual void insert(const std::string &input, const NodeValue &value) + { + (void) input; (void) value; + } + virtual void insert(const NodeValueRow &row) + { + (void) row; + } + virtual const NodeValueRow &get_values() const { static NodeValueRow r; return r; } + virtual NodeValueRow &get_values() { static NodeValueRow r; return r; } +}; +} diff --git a/src/node/transition/render/job/cachejob.h b/src/node/transition/render/job/cachejob.h new file mode 100644 index 000000000..da02cd13a --- /dev/null +++ b/src/node/transition/render/job/cachejob.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include "render/job/acceleratedjob.h" +namespace olive { +class CacheJob : public AcceleratedJob { +public: + CacheJob(const std::string &, const NodeValue &) {} + TexturePtr get_fallback() const { return nullptr; } +}; +} diff --git a/src/node/transition/render/job/colortransformjob.h b/src/node/transition/render/job/colortransformjob.h new file mode 100644 index 000000000..7094c4b7f --- /dev/null +++ b/src/node/transition/render/job/colortransformjob.h @@ -0,0 +1,16 @@ +#pragma once +#include "render/job/acceleratedjob.h" +#include "render/colorprocessor.h" +namespace olive { +class Node; +class ColorTransformJob : public AcceleratedJob { +public: + ColorTransformJob() = default; + ColorTransformJob(const NodeValueRow &) {} + NodeValue get_input_texture() const { return NodeValue(); } + void set_input_texture(const NodeValue &) {} + void set_color_processor(ColorProcessorPtr) {} + void set_needs_custom_shader(const Node *) {} + void set_function_name(const std::string &) {} +}; +} diff --git a/src/node/transition/render/job/footagejob.h b/src/node/transition/render/job/footagejob.h new file mode 100644 index 000000000..c555b7777 --- /dev/null +++ b/src/node/transition/render/job/footagejob.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "render/job/acceleratedjob.h" +#include "videoparams.h" +#include "olive/core/render/audioparams.h" +namespace olive { using core::AudioParams; } +#include "loopmode.h" +#include "olive/core/util/timerange.h" +#include "output/track/track.h" +namespace olive { +class FootageJob : public AcceleratedJob { +public: + FootageJob() {} + FootageJob(const core::TimeRange &, const std::string &, + const std::string &, Track::Type, const core::Rational &, + LoopMode) {} + void set_proxy(const std::string &, const std::string &, int) {} + void set_video_params(const VideoParams &) {} + void set_audio_params(const AudioParams &) {} + void set_cache_path(const std::string &) {} + core::TimeRange time() const { return core::TimeRange(); } + int loop_mode_as_int() const { return 0; } + LoopMode loop_mode() const { return LoopMode::k_loop_mode_off; } + core::Rational length() const { return core::Rational(); } + const VideoParams &video_params() const { static VideoParams p; return p; } +}; +} diff --git a/src/node/transition/render/job/generatejob.h b/src/node/transition/render/job/generatejob.h new file mode 100644 index 000000000..4d0772519 --- /dev/null +++ b/src/node/transition/render/job/generatejob.h @@ -0,0 +1,11 @@ +#pragma once +#include "render/job/acceleratedjob.h" +#include "value.h" +namespace olive { +class GenerateJob : public AcceleratedJob { +public: + GenerateJob() {} + explicit GenerateJob(const NodeValueRow &) {} + NodeValue get(const std::string &) const { return NodeValue(); } +}; +} diff --git a/src/node/transition/render/job/pluginjob.h b/src/node/transition/render/job/pluginjob.h new file mode 100644 index 000000000..51b1b2972 --- /dev/null +++ b/src/node/transition/render/job/pluginjob.h @@ -0,0 +1,16 @@ +#pragma once +#include "render/job/acceleratedjob.h" +#include "olive/core/util/rational.h" +namespace olive { +class Node; +namespace plugin { +class PluginJob : public AcceleratedJob { +public: + template + PluginJob(InstanceT *, const Node *, const NodeValueRow &, + const core::Rational &) + { + } +}; +} +} diff --git a/src/node/transition/render/job/samplejob.h b/src/node/transition/render/job/samplejob.h new file mode 100644 index 000000000..5a15d4bb9 --- /dev/null +++ b/src/node/transition/render/job/samplejob.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include "value.h" +#include "render/samplebuffer.h" +#include "olive/core/util/timerange.h" +namespace olive { +class SampleJob { +public: + SampleJob() = default; + SampleJob(const core::TimeRange &time, const NodeValue &value) + { + (void) time; (void) value; + } + SampleJob(const core::TimeRange &time, const std::string &from, + const NodeValueRow &values) + { + (void) time; (void) from; (void) values; + } + void insert(const std::string &input, const NodeValueRow &row) + { + (void) input; (void) row; + } + void insert(const std::string &input, const NodeValue &value) + { + (void) input; (void) value; + } + bool operator==(const SampleJob &) const { return true; } + const SampleBuffer &samples() const { static SampleBuffer b; return b; } + core::TimeRange time() const { return core::TimeRange(); } +}; +} diff --git a/src/node/transition/render/job/shaderjob.h b/src/node/transition/render/job/shaderjob.h new file mode 100644 index 000000000..4fe6b4c67 --- /dev/null +++ b/src/node/transition/render/job/shaderjob.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include "render/job/acceleratedjob.h" +namespace olive { +class ShaderJob : public AcceleratedJob { +public: + ShaderJob() = default; + ShaderJob(const NodeValueRow &row) { (void) row; } + NodeValue get(const std::string &id) const { (void) id; return NodeValue(); } + void set_shader_id(const std::string &id) { (void) id; } + void set_iterations(int iterations, const std::string &iterative_input) + { + (void) iterations; (void) iterative_input; + } + void set_interpolation(const std::string &id, int mode) + { + (void) id; (void) mode; + } + void set_vertex_coordinates(const std::vector &coords) + { + (void) coords; + } +}; +} diff --git a/src/node/transition/render/lutlibrary.h b/src/node/transition/render/lutlibrary.h new file mode 100644 index 000000000..b92a9ae1c --- /dev/null +++ b/src/node/transition/render/lutlibrary.h @@ -0,0 +1,15 @@ +#pragma once +// Transitional stub for engine/render/lutlibrary.h (still Qt-based). Only the +// surface oaknode uses. M7 replaces this with the real oakrender boundary. +#include "variant.h" +namespace olive { +class LUTLibrary { +public: + static const StringList &supported_extensions() + { + static const StringList e; + return e; + } + static bool is_supported_extension(const std::string &) { return false; } +}; +} diff --git a/src/node/transition/render/playbackcache.h b/src/node/transition/render/playbackcache.h new file mode 100644 index 000000000..9e3da13d5 --- /dev/null +++ b/src/node/transition/render/playbackcache.h @@ -0,0 +1,45 @@ +#pragma once +// Syntax-check stub only (not in repo). Minimal de-Qt shape of engine PlaybackCache. +#include +#include +#include "olive/core/util/timerange.h" +namespace olive { +class Node; +class ViewerOutput; +class PlaybackCache { +public: + template explicit PlaybackCache(T *) {} + virtual ~PlaybackCache() {} + void set_uuid(const std::string &) {} + std::string get_uuid() const { return {}; } + core::TimeRangeList get_invalidated_ranges(core::TimeRange intersecting) const + { + (void) intersecting; + return {}; + } + core::TimeRangeList get_invalidated_ranges(const core::Rational &length) const + { + return get_invalidated_ranges(core::TimeRange(0, length)); + } + void invalidate(const core::TimeRange &) {} + void invalidate_all() {} + void request(ViewerOutput *, const core::TimeRange &) {} + virtual void set_passthrough(PlaybackCache *) {} + class Passthrough : public core::TimeRange { + public: + Passthrough(const core::TimeRange &r, PlaybackCache *cache = nullptr) + : core::TimeRange(r) + , cache_(cache) + { + } + PlaybackCache *cache() const { return cache_; } + + private: + PlaybackCache *cache_; + }; + const std::vector &get_passthroughs() const { return passthroughs_; } + +private: + std::vector passthroughs_; +}; +} diff --git a/src/node/transition/render/previewautocacher.h b/src/node/transition/render/previewautocacher.h new file mode 100644 index 000000000..3f67a6205 --- /dev/null +++ b/src/node/transition/render/previewautocacher.h @@ -0,0 +1,12 @@ +#pragma once +// Transitional stub for engine/render/previewautocacher.h (still Qt-based). +// Only the surface oaknode uses. M7 replaces this with the real oakrender +// boundary. +namespace olive { +class PreviewAutoCacher { +public: + void cancel_video_tasks(bool) {} + void cancel_audio_tasks(bool) {} + void cancel_tasks(bool) {} +}; +} diff --git a/src/node/transition/render/rendermanager.h b/src/node/transition/render/rendermanager.h new file mode 100644 index 000000000..7b1b28d2f --- /dev/null +++ b/src/node/transition/render/rendermanager.h @@ -0,0 +1,12 @@ +#pragma once +// Transitional stub for engine/render/rendermanager.h (still Qt-based). Only +// the surface oaknode uses. M7 replaces this with the real oakrender +// boundary. +namespace olive { +class PreviewAutoCacher; +class RenderManager { +public: + static RenderManager *instance() { return nullptr; } + PreviewAutoCacher *get_cacher() { return nullptr; } +}; +} diff --git a/src/node/transition/render/samplebuffer.h b/src/node/transition/render/samplebuffer.h new file mode 100644 index 000000000..8656637dd --- /dev/null +++ b/src/node/transition/render/samplebuffer.h @@ -0,0 +1,6 @@ +#pragma once +// Transitional: SampleBuffer sunk to oakcore (olive::core::SampleBuffer, C ABI +// wrapper). Re-exported into namespace olive for node code. M7 will retarget +// this include to the real oakrender boundary. +#include "olive/core/render/samplebuffer.h" +namespace olive { using core::SampleBuffer; } diff --git a/src/node/transition/render/shadercode.h b/src/node/transition/render/shadercode.h new file mode 100644 index 000000000..a2d71f2b4 --- /dev/null +++ b/src/node/transition/render/shadercode.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include "filefunctions.h" +namespace olive { +class ShaderCode { +public: + ShaderCode(const std::string &frag = std::string(), const std::string &vert = std::string()) {} +}; +} diff --git a/src/node/transition/render/texture.h b/src/node/transition/render/texture.h new file mode 100644 index 000000000..24e96392c --- /dev/null +++ b/src/node/transition/render/texture.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include "videoparams.h" +#include "render/samplebuffer.h" +#include "olive/core/render/audioparams.h" +namespace olive { using core::AudioParams; } +namespace olive { +class AcceleratedJob; +class CacheJob; +class Texture; +using TexturePtr = std::shared_ptr; +class Texture { +public: + enum Interpolation { k_nearest, k_linear, k_mipmapped_linear }; + Texture(const VideoParams &) {} + int width() const { return 0; } + int height() const { return 0; } + int channel_count() const { return 0; } + core::Rational pixel_aspect_ratio() const { return core::Rational(); } + Vector2D virtual_resolution() const { return Vector2D(); } + AcceleratedJob *job() { return nullptr; } + template static TexturePtr job(const VideoParams &, const T &) { return nullptr; } + const VideoParams ¶ms() const { static VideoParams p; return p; } + template TexturePtr to_job(const T &) { return nullptr; } +}; +} diff --git a/src/node/transition/timeline/timelinecommon.h b/src/node/transition/timeline/timelinecommon.h new file mode 100644 index 000000000..baf2a6b57 --- /dev/null +++ b/src/node/transition/timeline/timelinecommon.h @@ -0,0 +1,22 @@ +#pragma once +#include "olive/core/util/rational.h" +namespace olive { +enum class TrackType { k_track_type_none, k_track_type_video, k_track_type_audio, k_track_type_subtitle, k_track_type_count }; +class Block; +class Track; +class Timeline { +public: + enum MovementMode { k_none, k_move, k_trim_in, k_trim_out }; + enum ThumbnailMode { k_thumbnail_off, k_thumbnail_in_out, k_thumbnail_on }; + enum WaveformMode { k_waveforms_disabled, k_waveforms_enabled }; + static bool is_a_trim_mode(MovementMode mode) + { + return mode == k_trim_in || mode == k_trim_out; + } + struct EditToInfo { + Track *track; + core::Rational nearest_time; + Block *nearest_block; + }; +}; +} diff --git a/src/node/transition/timeline/timelinemarker.h b/src/node/transition/timeline/timelinemarker.h new file mode 100644 index 000000000..242bf3554 --- /dev/null +++ b/src/node/transition/timeline/timelinemarker.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include "xmlutils.h" +#include "olive/core/util/timerange.h" +namespace olive { +using core::TimeRange; +class TimelineMarkerList { +public: + template explicit TimelineMarkerList(T *) {} + bool load(XmlStreamReader *) { return true; } + void save(XmlStreamWriter *) const {} +}; +class TimelineMarker { +public: + TimelineMarker() {} + explicit TimelineMarker(TimelineMarkerList *) {} + TimelineMarker(int, const TimeRange &, const std::string &, + TimelineMarkerList *) {} + void set_name(const std::string &) {} + void set_color(int) {} + void set_time(const TimeRange &) {} + bool load(XmlStreamReader *) { return true; } + void save(XmlStreamWriter *) const {} +}; +} diff --git a/src/node/transition/timeline/timelineundogeneral.h b/src/node/transition/timeline/timelineundogeneral.h new file mode 100644 index 000000000..4176025f1 --- /dev/null +++ b/src/node/transition/timeline/timelineundogeneral.h @@ -0,0 +1,15 @@ +#pragma once +// Transitional stub for engine/timeline/timelineundogeneral.h (still +// Qt-based). Only the surface oaknode uses. M4/M7 replace this with the real +// oaktimeline boundary. +#include "undocommand.h" +namespace olive { +class TrackList; +class TimelineAddTrackCommand : public UndoCommand { +public: + explicit TimelineAddTrackCommand(TrackList *) {} +protected: + void redo() override {} + void undo() override {} +}; +} diff --git a/src/node/transition/timeline/timelineworkarea.h b/src/node/transition/timeline/timelineworkarea.h new file mode 100644 index 000000000..74b62c148 --- /dev/null +++ b/src/node/transition/timeline/timelineworkarea.h @@ -0,0 +1,21 @@ +#pragma once +#include "xmlutils.h" +#include "olive/core/util/rational.h" +#include "olive/core/util/timerange.h" +namespace olive { +using core::Rational; +using core::TimeRange; +class TimelineWorkArea { +public: + template explicit TimelineWorkArea(T *) {} + const Rational &in() const { return in_; } + const Rational &out() const { return out_; } + void set_enabled(bool) {} + void set_range(const TimeRange &) {} + TimeRange range() const { return TimeRange(); } + bool load(XmlStreamReader *) { return true; } + void save(XmlStreamWriter *) const {} +private: + Rational in_, out_; +}; +} diff --git a/src/node/transition/ui/colorcoding.h b/src/node/transition/ui/colorcoding.h new file mode 100644 index 000000000..a1fb1abe8 --- /dev/null +++ b/src/node/transition/ui/colorcoding.h @@ -0,0 +1,8 @@ +#pragma once +#include "olive/core/util/color.h" +namespace olive { +class ColorCoding { +public: + static core::Color get_color(int) { return core::Color(); } +}; +}