diff --git a/react-native-harmony/.gitattributes b/react-native-harmony/.gitattributes new file mode 100644 index 000000000..d42ff1835 --- /dev/null +++ b/react-native-harmony/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/react-native-harmony/harmony/rn_babylon/.gitignore b/react-native-harmony/harmony/rn_babylon/.gitignore new file mode 100644 index 000000000..282a737bc --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test +./BuildProfile.ets diff --git a/react-native-harmony/harmony/rn_babylon/BuildProfile.ets b/react-native-harmony/harmony/rn_babylon/BuildProfile.ets new file mode 100644 index 000000000..fddad3658 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '0.0.1-0.0.1'; +export const BUILD_MODE_NAME = 'debug'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/build-profile.json5 b/react-native-harmony/harmony/rn_babylon/build-profile.json5 new file mode 100644 index 000000000..e35f3eed0 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/build-profile.json5 @@ -0,0 +1,12 @@ +{ + "apiType": 'stageMode', + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/hvigorfile.ts b/react-native-harmony/harmony/rn_babylon/hvigorfile.ts new file mode 100644 index 000000000..1d8c883b4 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/hvigorfile.ts @@ -0,0 +1,2 @@ +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { harTasks } from '@ohos/hvigor-ohos-plugin'; diff --git a/react-native-harmony/harmony/rn_babylon/index.ets b/react-native-harmony/harmony/rn_babylon/index.ets new file mode 100644 index 000000000..d65821fc3 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/index.ets @@ -0,0 +1,2 @@ +export * from "./ts" +// export * from "./src/main/ets/NativeEngineView" \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/oh-package-lock.json5 b/react-native-harmony/harmony/rn_babylon/oh-package-lock.json5 new file mode 100644 index 000000000..7b5d9a51a --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/oh-package-lock.json5 @@ -0,0 +1,20 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@rnoh/react-native-openharmony@0.72.48": "@rnoh/react-native-openharmony@0.72.48" + }, + "packages": { + "@rnoh/react-native-openharmony@0.72.48": { + "name": "@rnoh/react-native-openharmony", + "version": "0.72.48", + "integrity": "sha512-aoHhO8D6igaScj0njTKyMCAINaJZpW89+5JZhlLDX8c3ejQnph3wj+MoOlRQT0XPA2ikCpMkBbkgJz300i+FoA==", + "resolved": "https://repo.harmonyos.com/ohpm/@rnoh/react-native-openharmony/-/react-native-openharmony-0.72.48.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/oh-package.json5 b/react-native-harmony/harmony/rn_babylon/oh-package.json5 new file mode 100644 index 000000000..d7141145c --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/oh-package.json5 @@ -0,0 +1,13 @@ +{ + license: '', + devDependencies: {}, + author: '', + name: "bl", + description: '', + type: 'module', + version: '0.0.1-0.0.1', + main: 'index.ets', + dependencies: { + "@rnoh/react-native-openharmony": "^0.72.58", + }, +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/BabylonPackage.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/BabylonPackage.h new file mode 100644 index 000000000..724e91485 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/BabylonPackage.h @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_BABYLONPACKAGE_H +#define RN_BABYLON_SRC_MAIN_CPP_BABYLONPACKAGE_H + +#include "RnohReactNativeHarmonyBabylonPackage.h" + +namespace rnoh { +class BabylonPackage : public RnohReactNativeHarmonyBabylonPackage { + using Super = RnohReactNativeHarmonyBabylonPackage; + using Super::Super; +}; +} // namespace rnoh + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/CMakeLists.txt b/react-native-harmony/harmony/rn_babylon/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..db1463ef8 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +# Configure Babylon Native to use JSI +set(NAPI_JAVASCRIPT_ENGINE "JSI" CACHE STRING "The JavaScript engine to power N-API" FORCE) + +set(BABYLON_REACT_NATIVE_SHARED_DIR "${NODE_MODULES}/@react-native-oh-tpl/babylonjs-react-native/shared") + +include(${BABYLON_REACT_NATIVE_SHARED_DIR}/CMakeLists.txt) + +add_subdirectory("${BABYLON_REACT_NATIVE_SHARED_DIR}" ./babaylonjsshared) +add_subdirectory(${babylonnative_SOURCE_DIR} ./babylonnaitve) + +file(GLOB_RECURSE rnoh_babylon_SRC CONFIGURE_DEPENDS *.cpp) +add_library(rnoh_babylon SHARED ${rnoh_babylon_SRC} + ${SHARED_SOURCES} +) + +target_include_directories(rnoh_babylon PUBLIC + ${SHARED_INCLUDES} + ${CMAKE_CURRENT_SOURCE_DIR}) + +find_library( + EGL-lib + EGL +) + +find_library( + GLES-lib + GLESv3 +) + +target_link_libraries(rnoh_babylon PUBLIC rnoh + libace_ndk.z.so + libhilog_ndk.z.so + libnative_drawing.so + libnative_window.so + libffrt.z.so + ${EGL-lib} ${GLES-lib} + NativeInput + NativeOptimizations + GraphicsDevice + NativeTracing + NativeCamera + NativeXr + NativeCapture + NativeEngine + Window + Canvas + XMLHttpRequest + JsRuntime + pixelmap + image_packer +) \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/ComponentDescriptors.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ComponentDescriptors.h new file mode 100644 index 000000000..59f79764b --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ComponentDescriptors.h @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_COMPONENTDESCRIPTORS_H +#define RN_BABYLON_SRC_MAIN_CPP_COMPONENTDESCRIPTORS_H + +#include "ShadowNodes.h" +#include + +namespace facebook { + namespace react { + using EngineViewComponentDescriptor = ConcreteComponentDescriptor; + } +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.cpp new file mode 100644 index 000000000..3c124df55 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.cpp @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "EventEmitters.h" + +namespace facebook { + namespace react { + void EngineViewEventEmitter::onSnapshotDataReturned(SnapshotEventData event) const { + dispatchEvent("snapshotDataReturned", [event = std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "data", event.data); + return payload; + }); + } + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.h new file mode 100644 index 000000000..159102b2e --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/EventEmitters.h @@ -0,0 +1,44 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_EVENTEMITTERS_H +#define RN_BABYLON_SRC_MAIN_CPP_EVENTEMITTERS_H + +#include +#include + +namespace facebook { + namespace react { + class JSI_EXPORT EngineViewEventEmitter : public ViewEventEmitter { + public: + using ViewEventEmitter::ViewEventEmitter; + + struct SnapshotEventData { + std::string data; + }; + void onSnapshotDataReturned(SnapshotEventData value) const; + }; + } +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewJSIBinder.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewJSIBinder.h new file mode 100644 index 000000000..fb0523428 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewJSIBinder.h @@ -0,0 +1,60 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_NATIVEENGINEVIEWJSIBINDER_H +#define RN_BABYLON_SRC_MAIN_CPP_NATIVEENGINEVIEWJSIBINDER_H + +#include "RNOH/BaseComponentJSIBinder.h" +#include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h" + +namespace rnoh { + class NativeEngineViewJSIBinder : public ViewComponentJSIBinder { + protected: + facebook::jsi::Object createNativeProps(facebook::jsi::Runtime &rt) override { + auto object = ViewComponentJSIBinder::createNativeProps(rt); + object.setProperty(rt, "isTransparent", "boolean"); + object.setProperty(rt, "antiAliasing", "number"); + object.setProperty(rt, "androidView", "string"); + return object; + } + + facebook::jsi::Object createCommands(facebook::jsi::Runtime &rt) override { + auto commands = ViewComponentJSIBinder::createCommands(rt); + commands.setProperty(rt, "takeSnapshot", "takeSnapshot"); + return commands; + } + + facebook::jsi::Object createBubblingEventTypes(facebook::jsi::Runtime &rt) override { + facebook::jsi::Object events(rt); + return events; + } + + facebook::jsi::Object createDirectEventTypes(facebook::jsi::Runtime &rt) override { + facebook::jsi::Object events(rt); + events.setProperty(rt, "topSnapshotDataReturned", createDirectEvent(rt, "onSnapshotDataReturned")); + return events; + } + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewNapiBinder.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewNapiBinder.h new file mode 100644 index 000000000..fdbb253db --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/NativeEngineViewNapiBinder.h @@ -0,0 +1,49 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_NATIVEENGINEVIEWNAPIBINDER_H +#define RN_BABYLON_SRC_MAIN_CPP_NATIVEENGINEVIEWNAPIBINDER_H + +#include "Props.h" +#include "RNOH/BaseComponentNapiBinder.h" +#include "RNOHCorePackage/ComponentBinders/ViewComponentNapiBinder.h" + +namespace rnoh { + class NativeEngineViewNapiBinder : public ViewComponentNapiBinder { + public: + napi_value createProps(napi_env env, facebook::react::ShadowView const shadowView) override { + napi_value napiBaseProps = ViewComponentNapiBinder::createProps(env, shadowView); + if (auto props = std::dynamic_pointer_cast(shadowView.props)) { + return ArkJS(env) + .getObjectBuilder(napiBaseProps) + .addProperty("isTransparent", props->isTransparent) + .addProperty("antiAliasing", props->antiAliasing) + .addProperty("androidView", props->androidView) + .build(); + } + return napiBaseProps; + }; + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.cpp new file mode 100644 index 000000000..26e5c519a --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.cpp @@ -0,0 +1,37 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "Props.h" +#include +#include + +namespace facebook { + namespace react { + EngineViewProps::EngineViewProps(const PropsParserContext &context, const EngineViewProps &sourceProps, + const RawProps &rawProps) + : ViewProps(context, sourceProps, rawProps), + isTransparent(convertRawProp(context, rawProps, "isTransparent", sourceProps.isTransparent, {false})), + antiAliasing(convertRawProp(context, rawProps, "antiAliasing", sourceProps.antiAliasing, {0})), + androidView(convertRawProp(context, rawProps, "androidView", sourceProps.androidView, {""})) {} + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.h new file mode 100644 index 000000000..5ef8a75a5 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/Props.h @@ -0,0 +1,50 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_PROPS_H +#define RN_BABYLON_SRC_MAIN_CPP_PROPS_H + +#include +#include +#include +#include +#include +#include + +namespace facebook { + namespace react { + class JSI_EXPORT EngineViewProps final : public ViewProps { + public: + EngineViewProps() = default; + EngineViewProps(const PropsParserContext &context, const EngineViewProps &sourceProps, const RawProps &rawProps); + + #pragma mark - Props + + bool isTransparent{true}; + int32_t antiAliasing{0}; + std::string androidView{""}; + }; + } +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.cpp new file mode 100644 index 000000000..0c3852cfc --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.cpp @@ -0,0 +1,64 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "RNBabylonModule.h" + +namespace rnoh { + using namespace facebook; + + jsi::Value initialize(facebook::jsi::Runtime &rt, react::TurboModule &turboModule, const facebook::jsi::Value *args, + size_t count) { + auto self = static_cast(&turboModule); + auto jsDispatcher = self->createJsDispatcher(self->getContext()); + return facebook::react::createPromiseAsJSIValue( + rt, [&jsDispatcher](jsi::Runtime &runtime, std::shared_ptr promise) { + BabylonNative::Initialize(runtime, jsDispatcher); + promise->resolve(jsi::Value().null()); + }); + } + + jsi::Value resetView(facebook::jsi::Runtime &rt, react::TurboModule &turboModule, const facebook::jsi::Value *args, + size_t count) { + auto self = static_cast(&turboModule); + return facebook::react::createPromiseAsJSIValue( + rt, [self](jsi::Runtime &runtime, std::shared_ptr promise) { + auto jsDispatcher = self->createMainDispatcher(self->getContext()); + jsDispatcher([promise]() { + BabylonNative::ResetView(); + promise->resolve(jsi::Value().null()); + }); + }); + } + + RNBabylonModule::RNBabylonModule(const ArkTSTurboModule::Context ctx, const std::string name) + : ArkTSTurboModule(ctx, name) { + + methodMap_ = { + {"initialize", {0, rnoh::initialize}}, + {"resetView", {0, rnoh::resetView}}, + ARK_METHOD_METADATA(addListener, 1), + ARK_METHOD_METADATA(removeListeners, 1), + }; + std::thread::id this_id = std::this_thread::get_id(); + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.h new file mode 100644 index 000000000..85e240e08 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonModule.h @@ -0,0 +1,55 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_RNBABYLONMODULE_H +#define RN_BABYLON_SRC_MAIN_CPP_RNBABYLONMODULE_H + +#include "BabylonNative.h" +#include "RNOH/ArkTSTurboModule.h" + +namespace rnoh { + class JSI_EXPORT RNBabylonModule : public ArkTSTurboModule { + public: + RNBabylonModule(const ArkTSTurboModule::Context ctx, const std::string name); + + BabylonNative::Dispatcher createJsDispatcher(Context context) { + return [context](std::function job) { + if (!context.taskExecutor) { + return; + } + context.taskExecutor->runTask(TaskThread::JS, [job = std::move(job)]() { job(); }); + }; + } + + BabylonNative::Dispatcher createMainDispatcher(Context context) { + return [context](std::function job) { + if (!context.taskExecutor) { + return; + } + context.taskExecutor->runTask(TaskThread::MAIN, [job = std::move(job)]() { job(); }); + }; + } + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonNativeBridge.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonNativeBridge.h new file mode 100644 index 000000000..600948865 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNBabylonNativeBridge.h @@ -0,0 +1,66 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_RNBABYLONNATIVEBRIDGE_H +#define RN_BABYLON_SRC_MAIN_CPP_RNBABYLONNATIVEBRIDGE_H + +#include "stdint.h" +#include "BabylonNative.h" + +namespace rnoh { + void BabyUpdateView(OHNativeWindow *window, uint64_t width, uint64_t height) { + BabylonNative::UpdateView(window, width, height); + } + + void BabyUpdateMSAA(int val) { + BabylonNative::UpdateMSAA(static_cast(val)); + } + + void BabyRenderView() { + BabylonNative::RenderView(); + } + + void BabyResetView() { + BabylonNative::ResetView(); + } + + void BabyUpdateXRView(OHNativeWindow *window) { + BabylonNative::UpdateXRView(window); + } + + bool BabyIsXRActive() { + return BabylonNative::IsXRActive(); + } + + void BabySetTouchButtonState(int pointerId, bool isDown, float x, float y) { + BabylonNative::SetTouchButtonState(static_cast(pointerId), isDown, static_cast(x), + static_cast(y)); + } + + void BabySetTouchPosition(int pointerId, float x, float y) { + BabylonNative::SetTouchPosition(static_cast(pointerId), static_cast(x), + static_cast(y)); + } +}; + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.cpp new file mode 100644 index 000000000..e605f2228 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.cpp @@ -0,0 +1,318 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "RNCEngineNode.h" +#include "RNBabylonNativeBridge.h" +#include "RNOH/arkui/NativeNodeApi.h" +#include + +namespace rnoh { + void RNCEngineNode::OnSurfaceCreated(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnSurfaceCreated"; + if (window && component) { + uint64_t width = 0, height = 0; + int status = OH_NativeXComponent_GetXComponentSize(component, window, &width, &height); + if (status == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + auto node = getNode(component); + if (node) { + static_cast(node)->UpdateView(window, width, height); + static_cast(node)->startRenderLoop(); + } + } + } + } + + void RNCEngineNode::OnSurfaceChanged(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnSurfaceChanged"; + if (window && component) { + uint64_t width = 0, height = 0; + int status = OH_NativeXComponent_GetXComponentSize(component, window, &width, &height); + if (status == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + auto node = getNode(component); + if (node) { + static_cast(node)->UpdateView(window, width, height); + } + } + } + } + + void RNCEngineNode::OnSurfaceDestroyed(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnSurfaceDestroyed"; + auto node = getNode(component); + if (node) { + static_cast(node)->stopRenderLoop(); + } + } + + void RNCEngineNode::DispatchTouchEvent(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon DispatchTouchEvent"; + if (component && window) { + auto node = getNode(component); + if (node) { + static_cast(node)->reportMotionEvent(component, window); + } + } + } + + void RNCEngineNode::OnXRSurfaceCreated(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnXRSurfaceCreated"; + if (component && window) { + auto node = getNode(component); + if (node) { + static_cast(node)->UpdateXRView(window); + } + } + } + + void RNCEngineNode::OnXRSurfaceChanged(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnXRSurfaceChanged"; + if (component && window) { + auto node = getNode(component); + if (node) { + static_cast(node)->UpdateXRView(window); + } + } + } + + void RNCEngineNode::OnXRSurfaceDestroyed(OH_NativeXComponent *component, void *window) { + DLOG(INFO) << "RNBabylon OnXRSurfaceDestroyed"; + auto node = getNode(component); + if (node) { + static_cast(node)->UpdateXRView(nullptr); + } + } + + void RNCEngineNode::onFrameCallback(OH_NativeXComponent *component, uint64_t timestamp, uint64_t targetTimestamp) { + auto *instance = static_cast(getNode(component)); + if (!instance) { + return; + } + + // 切换XR + bool isXR = instance->isXRActive(); + ArkUI_NumberValue visible[] = {{.u32 = isXR ? ARKUI_VISIBILITY_VISIBLE : ARKUI_VISIBILITY_NONE}}; + ArkUI_AttributeItem visibleAttr = {visible, 1}; + NativeNodeApi::getInstance()->setAttribute(instance->m_xrNodeHandle, NODE_VISIBILITY, &visibleAttr); + + instance->renderView(); + + // 继续注册下一帧回调?(保持循环) + if (instance->m_IsRendering) { // TODO + OH_NativeXComponent_RegisterOnFrameCallback(component, onFrameCallback); + } + } + + RNCEngineNode::RNCEngineNode() : ArkUINode(NativeNodeApi::getInstance()->createNode(ArkUI_NodeType::ARKUI_NODE_STACK)) { + createGeneralXComponent(); + createXRXComponent(); + + // 添加 + NativeNodeApi::getInstance()->insertChildAt(m_nodeHandle, m_General_NodeHandle, 0); + NativeNodeApi::getInstance()->insertChildAt(m_nodeHandle, m_xrNodeHandle, 1); // XR保持在最上层 + + if (m_nativeXComponent) { + std::lock_guard lock(s_Mutex); + s_componentMap[m_nativeXComponent] = this; + s_componentMap[m_xr_nativeXComponent] = this; + } + } + + RNCEngineNode::~RNCEngineNode() { + stopRenderLoop(); + if (m_General_NodeHandle) { + NativeNodeApi::getInstance()->removeChild(m_nodeHandle, m_General_NodeHandle); + NativeNodeApi::getInstance()->disposeNode(m_General_NodeHandle); + m_General_NodeHandle = nullptr; + } + if (m_xrNodeHandle) { + NativeNodeApi::getInstance()->removeChild(m_nodeHandle, m_xrNodeHandle); + NativeNodeApi::getInstance()->disposeNode(m_xrNodeHandle); + m_xrNodeHandle = nullptr; + } + if (m_nativeXComponent != nullptr || m_xr_nativeXComponent != nullptr) { + std::lock_guard lock(s_Mutex); + s_componentMap.erase(m_nativeXComponent); + s_componentMap.erase(m_xr_nativeXComponent); + m_nativeXComponent = nullptr; + m_xr_nativeXComponent = nullptr; + } + } + + void RNCEngineNode::createGeneralXComponent() { + m_General_NodeHandle = NativeNodeApi::getInstance()->createNode(ArkUI_NodeType::ARKUI_NODE_XCOMPONENT); + + m_xComponentCallback_.OnSurfaceCreated = OnSurfaceCreated; + m_xComponentCallback_.OnSurfaceChanged = OnSurfaceChanged; + m_xComponentCallback_.OnSurfaceDestroyed = OnSurfaceDestroyed; + m_xComponentCallback_.DispatchTouchEvent = DispatchTouchEvent; + + ArkUI_NumberValue surfaceTypeValue[] = {{.u32 = ARKUI_XCOMPONENT_TYPE_SURFACE}}; + ArkUI_AttributeItem surfaceType = {surfaceTypeValue, sizeof(surfaceTypeValue) / sizeof(ArkUI_NumberValue)}; + NativeNodeApi::getInstance()->setAttribute(m_General_NodeHandle, NODE_XCOMPONENT_TYPE, &surfaceType); + + m_nativeXComponent = OH_NativeXComponent_GetNativeXComponent(m_General_NodeHandle); + OH_NativeXComponent_RegisterCallback(m_nativeXComponent, &m_xComponentCallback_); + } + + void RNCEngineNode::createXRXComponent() { + // XR + m_xrNodeHandle = NativeNodeApi::getInstance()->createNode(ArkUI_NodeType::ARKUI_NODE_XCOMPONENT); + m_xrXComponentCallback.OnSurfaceCreated = OnXRSurfaceCreated; + m_xrXComponentCallback.OnSurfaceChanged = OnXRSurfaceChanged; + m_xrXComponentCallback.OnSurfaceDestroyed = OnXRSurfaceDestroyed; + m_xrXComponentCallback.DispatchTouchEvent = [](OH_NativeXComponent *, void *) {}; + + ArkUI_NumberValue surfaceTypeValue[] = {{.u32 = ARKUI_XCOMPONENT_TYPE_SURFACE}}; + ArkUI_AttributeItem surfaceType = {surfaceTypeValue, sizeof(surfaceTypeValue) / sizeof(ArkUI_NumberValue)}; + NativeNodeApi::getInstance()->setAttribute(m_xrNodeHandle, NODE_XCOMPONENT_TYPE, &surfaceType); + + m_xr_nativeXComponent = OH_NativeXComponent_GetNativeXComponent(m_xrNodeHandle); + OH_NativeXComponent_RegisterCallback(m_xr_nativeXComponent, &m_xrXComponentCallback); + + // 先隐藏XR + ArkUI_NumberValue visible[] = {{.u32 = ARKUI_VISIBILITY_NONE}}; + ArkUI_AttributeItem visibleAttr = {visible, 1}; + NativeNodeApi::getInstance()->setAttribute(m_xrNodeHandle, NODE_VISIBILITY, &visibleAttr); + } + + void RNCEngineNode::setAntiAliasing(int32_t value) { + UpdateMSAA(value); + } + void RNCEngineNode::setAndroidView(std::string androidView) { + setIsTransparentAndAndroidView(m_IsTransparent, androidView); + } + void RNCEngineNode::setIsTransparent(bool isTransparent) { + setIsTransparentAndAndroidView(isTransparent, m_View); + } + + RNCEngineNode *RNCEngineNode::getNode(OH_NativeXComponent *component) { + if (component == nullptr) { + return nullptr; + } + std::lock_guard lock(s_Mutex); + auto it = s_componentMap.find(component); + if (it != s_componentMap.end()) { + return it->second; + } + return nullptr; + } + + void RNCEngineNode::startRenderLoop() { + if (m_IsRendering) { + return; + } + m_IsRendering = true; + auto *nativeXComponent = OH_NativeXComponent_GetNativeXComponent(m_General_NodeHandle); + OH_NativeXComponent_RegisterOnFrameCallback(nativeXComponent, onFrameCallback); + } + + void RNCEngineNode::stopRenderLoop() { + if (!m_IsRendering) { + return; + } + m_IsRendering = false; + auto *nativeXComponent = OH_NativeXComponent_GetNativeXComponent(m_General_NodeHandle); + if (nativeXComponent) { + OH_NativeXComponent_UnregisterOnFrameCallback(nativeXComponent); + } + } + + void RNCEngineNode::setIsTransparentAndAndroidView(bool transparent, std::string view) { + if (m_IsTransparent == transparent && m_View == view) { + return; + } + m_IsTransparent = transparent; + m_View = view; + + if (transparent) { + // 设置XComponent背景透明 0x00000000 + ArkUI_NumberValue preparedColorValue[] = {{.u32 = 0x00000000}}; + ArkUI_AttributeItem colorItem = {preparedColorValue, sizeof(preparedColorValue) / sizeof(ArkUI_NumberValue)}; + NativeNodeApi::getInstance()->setAttribute(m_nodeHandle, NODE_BACKGROUND_COLOR, &colorItem); + } + + if (view == "SurfaceViewZTopMost") { // 指定层级 + ArkUI_NumberValue zOrder[] = {{.i32 = 1000}}; // TODO 1000 与 500 + ArkUI_AttributeItem zAttr = {zOrder, 1}; + NativeNodeApi::getInstance()->setAttribute(m_nodeHandle, NODE_Z_INDEX, &zAttr); + } else if (view == "SurfaceViewZMediaOverlay") { + ArkUI_NumberValue zOrder[] = {{.i32 = 500}}; + ArkUI_AttributeItem zAttr = {zOrder, 1}; + NativeNodeApi::getInstance()->setAttribute(m_nodeHandle, NODE_Z_INDEX, &zAttr); + } + + if (view == "TextureView") { + ArkUI_NumberValue type[] = {{.u32 = ARKUI_XCOMPONENT_TYPE_TEXTURE}}; + ArkUI_AttributeItem typeAttr = {type, 1}; + NativeNodeApi::getInstance()->setAttribute(m_General_NodeHandle, NODE_XCOMPONENT_TYPE, &typeAttr); + } else { + ArkUI_NumberValue type[] = {{.u32 = ARKUI_XCOMPONENT_TYPE_SURFACE}}; + ArkUI_AttributeItem typeAttr = {type, 1}; + NativeNodeApi::getInstance()->setAttribute(m_General_NodeHandle, NODE_XCOMPONENT_TYPE, &typeAttr); + } + } + + void RNCEngineNode::UpdateView(void *window, uint64_t width, uint64_t height) { + BabyUpdateView(static_cast(window), width, height); + } + + void RNCEngineNode::UpdateXRView(void *window) { + BabyUpdateXRView(static_cast(window)); + } + + void RNCEngineNode::UpdateMSAA(int32_t value) { + BabyUpdateMSAA(value); + } + + bool RNCEngineNode::isXRActive() { + return BabyIsXRActive(); + } + + void RNCEngineNode::renderView() { + BabyRenderView(); + } + + void RNCEngineNode::reportMotionEvent(OH_NativeXComponent *component, void *window) { + OH_NativeXComponent_TouchEvent event{}; + if (OH_NativeXComponent_GetTouchEvent(component, window, &event) == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + bool isPointerDown = event.type == OH_NATIVEXCOMPONENT_DOWN; + bool isPointerUp = event.type == OH_NATIVEXCOMPONENT_UP; + bool isPointerMove = event.type == OH_NATIVEXCOMPONENT_MOVE; + if (isPointerDown || isPointerUp) { + int pointerId = event.id; + int x = event.x; + int y = event.y; + BabySetTouchButtonState(pointerId, isPointerDown, x, y); + } else if (isPointerMove) { + auto touchPoints = event.touchPoints; + int length = sizeof(touchPoints) / sizeof(touchPoints[0]); + for (int pointerIndex = 0; pointerIndex < length; pointerIndex++) { + int pointerId = touchPoints[pointerIndex].id; + int x = touchPoints[pointerIndex].x; + int y = touchPoints[pointerIndex].y; + BabySetTouchPosition(pointerId, x, y); + } + } + } + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.h new file mode 100644 index 000000000..971571cb8 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCEngineNode.h @@ -0,0 +1,81 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_RNCENGINENODE_H +#define RN_BABYLON_SRC_MAIN_CPP_RNCENGINENODE_H + +#include "RNOH/arkui/ArkUINode.h" +#include + +namespace rnoh { + class RNCEngineNode : public ArkUINode { + public: + RNCEngineNode(); + ~RNCEngineNode() override; + + // 普通 + static void OnSurfaceCreated(OH_NativeXComponent *component, void *window); + static void OnSurfaceChanged(OH_NativeXComponent *component, void *window); + static void OnSurfaceDestroyed(OH_NativeXComponent *component, void *window); + static void DispatchTouchEvent(OH_NativeXComponent *component, void *window); + + // XR + static void OnXRSurfaceCreated(OH_NativeXComponent *component, void *window); + static void OnXRSurfaceChanged(OH_NativeXComponent *component, void *window); + static void OnXRSurfaceDestroyed(OH_NativeXComponent *component, void *window); + + void setAntiAliasing(int32_t value); + void setAndroidView(std::string androidView); + void setIsTransparent(bool isTransparent); + + private: + void setIsTransparentAndAndroidView(bool isTransparent, std::string androidView); + void startRenderLoop(); + void stopRenderLoop(); + static void onFrameCallback(OH_NativeXComponent *component, uint64_t timestamp, uint64_t targetTimestamp); + static RNCEngineNode *getNode(OH_NativeXComponent *component); + void UpdateView(void *window, uint64_t width, uint64_t height); + void UpdateXRView(void *window); + void UpdateMSAA(int32_t value); + bool isXRActive(); + void renderView(); + void reportMotionEvent(OH_NativeXComponent *component, void *window); + void createGeneralXComponent(); + void createXRXComponent(); + + OH_NativeXComponent_Callback m_xComponentCallback_; + OH_NativeXComponent_Callback m_xrXComponentCallback; + ArkUI_NodeHandle m_General_NodeHandle{nullptr}; + ArkUI_NodeHandle m_xrNodeHandle{nullptr}; + bool m_IsRendering{false}; + bool m_IsTransparent{false}; + std::string m_View{""}; + OH_NativeXComponent *m_nativeXComponent{nullptr}; + OH_NativeXComponent *m_xr_nativeXComponent{nullptr}; + + static inline std::unordered_map s_componentMap; + static inline std::mutex s_Mutex; + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.cpp new file mode 100644 index 000000000..454279682 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.cpp @@ -0,0 +1,253 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include +#include +#include +#include +#include "RNCNativeEngineViewComponentInstance.h" + +namespace rnoh { + RNCNativeEngineViewComponentInstance::RNCNativeEngineViewComponentInstance(Context context) + : CppComponentInstance(std::move(context)) {} + + RNCNativeEngineViewComponentInstance::~RNCNativeEngineViewComponentInstance() {} + + void RNCNativeEngineViewComponentInstance::onChildInserted(ComponentInstance::Shared const &childComponentInstance, + std::size_t index) { + CppComponentInstance::onChildInserted(childComponentInstance, index); + auto mWidth = childComponentInstance->getLayoutMetrics().frame.size.width; + } + + void RNCNativeEngineViewComponentInstance::onChildRemoved(ComponentInstance::Shared const &childComponentInstance) { + CppComponentInstance::onChildRemoved(childComponentInstance); + }; + + RNCEngineNode &RNCNativeEngineViewComponentInstance::getLocalRootArkUINode() { + return m_EngineNode; + } + + void RNCNativeEngineViewComponentInstance::onPropsChanged(SharedConcreteProps const &props) { + CppComponentInstance::onPropsChanged(props); + if (props == nullptr) { + return; + } + + DLOG(INFO) << "onPropsChanged antiAliasing:" << props->antiAliasing; + DLOG(INFO) << "onPropsChanged isTransparent:" << props->isTransparent; + DLOG(INFO) << "onPropsChanged androidView:" << props->androidView; + + if (props->antiAliasing) { + m_EngineNode.setAntiAliasing(props->antiAliasing); + } + if (props->isTransparent) { + m_EngineNode.setIsTransparent(props->isTransparent); + } + m_EngineNode.setAndroidView(props->androidView); + } + + void RNCNativeEngineViewComponentInstance::onSnapshotDataReturned(std::string data) { + if (m_eventEmitter) { + DLOG(INFO) << "onSnapshotDataReturned send data"; + m_eventEmitter->onSnapshotDataReturned({data}); + } + }; + + static const std::string base64Chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + std::string base64Encode(const uint8_t* data, size_t size) { + std::string ret; + int i = 0; + int j = 0; + unsigned char charArray3[3]; + unsigned char charArray4[4]; + + while (size--) { + charArray3[i++] = *(data++); + if (i == 3) { + charArray4[0] = (charArray3[0] & 0xfc) >> 2; + charArray4[1] = ((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4); + charArray4[2] = ((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6); + charArray4[3] = charArray3[2] & 0x3f; + + for(i = 0; i < 4 ; i++) { + ret += base64Chars[charArray4[i]]; + } + + i = 0; + } + } + + if (i) { + for(j = i; j < 3; j++) { + charArray3[j] = '\0'; + } + + charArray4[0] = (charArray3[0] & 0xfc) >> 2; + charArray4[1] = ((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4); + charArray4[2] = ((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6); + charArray4[3] = charArray3[2] & 0x3f; + + for (j = 0; j < i + 1; j++) { + ret += base64Chars[charArray4[j]]; + } + + while (i++ < 3) { + ret += '='; + } + } + + return ret; + } + + void RNCNativeEngineViewComponentInstance::onSnapshoting() { + OH_PixelmapNative *pixelmap = nullptr; + OH_Pixelmap_ImageInfo *imageInfo = nullptr; + OH_ImagePackerNative *imagePacker = nullptr; + std::string base64Result = ""; + + try { + // 获取节点快照 + int32_t code = OH_ArkUI_GetNodeSnapshot(m_EngineNode.getArkUINodeHandle(), nullptr, &pixelmap); + if (code != ARKUI_ERROR_CODE_NO_ERROR) { + DLOG(ERROR) << "OH_ArkUI_GetNodeSnapshot failfail, errCode: " << code; + throw std::runtime_error("OH_ArkUI_GetNodeSnapshot fail"); + } + + // 创建图像信息对象 + Image_ErrorCode errCode = OH_PixelmapImageInfo_Create(&imageInfo); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapImageInfo_Create fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapImageInfo_Create fail"); + } + + // 获取图像信息 + errCode = OH_PixelmapNative_GetImageInfo(pixelmap, imageInfo); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapNative_GetImageInfo fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapNative_GetImageInfo fail"); + } + + // 获取宽度 + uint32_t width; + errCode = OH_PixelmapImageInfo_GetWidth(imageInfo, &width); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapImageInfo_GetWidth fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapImageInfo_GetWidth fail"); + } + + // 获取高度 + uint32_t height; + errCode = OH_PixelmapImageInfo_GetHeight(imageInfo, &height); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapImageInfo_GetHeight fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapImageInfo_GetHeight fail"); + } + + // 获取像素格式 + int32_t pixelFormat; + errCode = OH_PixelmapImageInfo_GetPixelFormat(imageInfo, &pixelFormat); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapImageInfo_GetPixelFormat fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapImageInfo_GetPixelFormat fail"); + } + + // 检查图像尺寸是否有效 + if (width == 0 || height == 0) { + DLOG(ERROR) << "Invalid image size: " << width << "x" << height; + throw std::runtime_error("Invalid image size"); + } + + // 分配像素数据缓冲区(RGBA格式,每像素4字节) + size_t bufferSize = width * height * 4; + std::vector destination(bufferSize); + + // 读取像素数据 + errCode = OH_PixelmapNative_ReadPixels(pixelmap, destination.data(), &bufferSize); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_PixelmapNative_ReadPixels fail, errCode: " << errCode; + throw std::runtime_error("OH_PixelmapNative_ReadPixels fail"); + } + + // 创建ImagePacker实例 + errCode = OH_ImagePackerNative_Create(&imagePacker); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_ImagePackerNative_Create fail, errCode: " << errCode; + throw std::runtime_error("OH_ImagePackerNative_Create fail"); + } + + // 将PixelMap编码成JPEG格式数据 + OH_PackingOptions *option = nullptr; + OH_PackingOptions_Create(&option); + char type[] = "image/jpeg"; + Image_MimeType image_MimeType = {type, strlen(type)}; + OH_PackingOptions_SetMimeType(option, &image_MimeType); + + size_t imageSize = 0; + std::vector image(bufferSize); + errCode = OH_ImagePackerNative_PackToDataFromPixelmap(imagePacker, option, pixelmap, image.data(), &imageSize); + if (errCode != IMAGE_SUCCESS) { + DLOG(ERROR) << "OH_ImagePackerNative_PackToDataFromPixelmap fail, errCode: " << errCode; + throw std::runtime_error("OH_ImagePackerNative_PackToDataFromPixelmap fail"); + } + + // 转换为Base64编码 + base64Result = base64Encode(image.data(), imageSize); + if (base64Result.empty()) { + DLOG(ERROR) << "base64Encode fail"; + throw std::runtime_error("base64Encode fail"); + } + + DLOG(INFO) << "Snapshort success, width: " << width << ", height: " << height << ", size: " << base64Result.length(); + } catch (const std::exception& e) { + DLOG(ERROR) << "Snapshort exception: " << e.what(); + base64Result = ""; // 确保返回空字符串表示失败 + } + + if (imagePacker != nullptr) { + OH_ImagePackerNative_Release(imagePacker); + } + + if (imageInfo != nullptr) { + OH_PixelmapImageInfo_Release(imageInfo); + } + + if (pixelmap != nullptr) { + OH_PixelmapNative_Release(pixelmap); + } + + // 发送结果到React Native层 + this->onSnapshotDataReturned(base64Result); + }; + + void RNCNativeEngineViewComponentInstance::handleCommand(std::string const &commandName, folly::dynamic const &args) { + DLOG(INFO) << "handleCommand:" << commandName; + if (commandName == "takeSnapshot") { + this->onSnapshoting(); + } + }; + +}; \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.h new file mode 100644 index 000000000..3879f61ee --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RNCNativeEngineViewComponentInstance.h @@ -0,0 +1,49 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_RNCNATIVEENGINEVIEWCOMPONENTINSTANCE_H +#define RN_BABYLON_SRC_MAIN_CPP_RNCNATIVEENGINEVIEWCOMPONENTINSTANCE_H + +#include "RNCEngineNode.h" +#include "RNOH/CppComponentInstance.h" +#include "ShadowNodes.h" + +namespace rnoh { + class RNCNativeEngineViewComponentInstance : public rnoh::CppComponentInstance { + public: + RNCNativeEngineViewComponentInstance(Context context); + ~RNCNativeEngineViewComponentInstance(); + void onChildInserted(ComponentInstance::Shared const &childComponentInstance, std::size_t index) override; + void onChildRemoved(ComponentInstance::Shared const &childComponentInstance) override; + void onSnapshoting(); + void onPropsChanged(SharedConcreteProps const &props) override; + void handleCommand(std::string const &commandName, folly::dynamic const &args) override; + RNCEngineNode &getLocalRootArkUINode() override; + void onSnapshotDataReturned(std::string data) ; + + private: + RNCEngineNode m_EngineNode; + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.cpp new file mode 100644 index 000000000..c87756cc3 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.cpp @@ -0,0 +1,109 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "RnohReactNativeHarmonyBabylonPackage.h" +#include "ComponentDescriptors.h" +#include "NativeEngineViewJSIBinder.h" +#include "NativeEngineViewNapiBinder.h" +#include "RNBabylonModule.h" +#include "RNCNativeEngineViewComponentInstance.h" +#include "RNOH/ArkTSTurboModule.h" +#include + +using namespace rnoh; +using namespace facebook; + +class BabylonPackageComponentInstanceFactoryDelegate : public ComponentInstanceFactoryDelegate { +public: + using ComponentInstanceFactoryDelegate::ComponentInstanceFactoryDelegate; + + ComponentInstance::Shared create(ComponentInstance::Context ctx) override { + if (ctx.componentName == "NativeEngineView") { + return std::make_shared(std::move(ctx)); + } + return nullptr; + } +}; + +class BabylonTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate { +public: + SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override { + if (name == "NativeRNBabylonModule") { + return std::make_shared(ctx, name); + } + return nullptr; + }; +}; + +std::unique_ptr RnohReactNativeHarmonyBabylonPackage::createTurboModuleFactoryDelegate() { + return std::make_unique(); +} + +ComponentInstanceFactoryDelegate::Shared +RnohReactNativeHarmonyBabylonPackage::createComponentInstanceFactoryDelegate() { + return std::make_shared(); +} + +std::vector +RnohReactNativeHarmonyBabylonPackage::createComponentDescriptorProviders() { + return {facebook::react::concreteComponentDescriptorProvider()}; +} + +ComponentJSIBinderByString RnohReactNativeHarmonyBabylonPackage::createComponentJSIBinderByName() { + return { + {"NativeEngineView", std::make_shared()}, + }; +} + +ComponentNapiBinderByString RnohReactNativeHarmonyBabylonPackage::createComponentNapiBinderByName() { + return { + {"NativeEngineView", std::make_shared()}, + }; +} + +class EngineViewEventEmitRequestHandler : public EventEmitRequestHandler { + void handleEvent(EventEmitRequestHandler::Context const &ctx) override { + DLOG(INFO) << "handleEvent eventName:" << ctx.eventName; + + if (ctx.eventName != "NativeEngineView") { + return; + } + + facebook::react::SystraceSection s("RNGH::BabylonEventEmitRequestHandler::handleEvent"); + auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter(ctx.tag); + if (eventEmitter == nullptr) { + return; + } + + std::vector supportedEventNames = {}; + if (std::find(supportedEventNames.begin(), supportedEventNames.end(), ctx.eventName) != supportedEventNames.end()) { + eventEmitter->dispatchEvent(ctx.eventName, ArkJS(ctx.env).getDynamic(ctx.payload)); + } + } +}; + +EventEmitRequestHandlers RnohReactNativeHarmonyBabylonPackage::createEventEmitRequestHandlers() { + return { + std::make_shared(), + }; +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.h new file mode 100644 index 000000000..9cf616ca9 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/RnohReactNativeHarmonyBabylonPackage.h @@ -0,0 +1,42 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_RNOHREACTNATIVEHARMONYBABYLONPACKAGE_H +#define RN_BABYLON_SRC_MAIN_CPP_RNOHREACTNATIVEHARMONYBABYLONPACKAGE_H + +#include "RNOH/Package.h" + +namespace rnoh { + class RnohReactNativeHarmonyBabylonPackage : public Package { + public: + RnohReactNativeHarmonyBabylonPackage(Package::Context ctx) : Package(ctx) {} + std::unique_ptr createTurboModuleFactoryDelegate() override; + EventEmitRequestHandlers createEventEmitRequestHandlers() override; + ComponentInstanceFactoryDelegate::Shared createComponentInstanceFactoryDelegate() override; + std::vector createComponentDescriptorProviders() override; + ComponentJSIBinderByString createComponentJSIBinderByName() override; + ComponentNapiBinderByString createComponentNapiBinderByName() override; + }; +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.cpp new file mode 100644 index 000000000..207020484 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.cpp @@ -0,0 +1,30 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "ShadowNodes.h" + +namespace facebook { + namespace react { + extern const char EngineViewComponentName[] = "NativeEngineView"; + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.h new file mode 100644 index 000000000..211f27e24 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/ShadowNodes.h @@ -0,0 +1,46 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_SHADOWNODES_H +#define RN_BABYLON_SRC_MAIN_CPP_SHADOWNODES_H + +#include "EventEmitters.h" +#include "Props.h" +#include "States.h" +#include +#include + +namespace facebook { + namespace react { + JSI_EXPORT extern const char EngineViewComponentName[]; + + using EngineViewShadowNode = ConcreteViewShadowNode< + EngineViewComponentName, + EngineViewProps, + EngineViewEventEmitter, + EngineViewState + >; + } +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.cpp b/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.cpp new file mode 100644 index 000000000..1ec42ff8a --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.cpp @@ -0,0 +1,24 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "States.h" \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.h b/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.h new file mode 100644 index 000000000..6ce61c02b --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/cpp/States.h @@ -0,0 +1,36 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef RN_BABYLON_SRC_MAIN_CPP_STATES_H +#define RN_BABYLON_SRC_MAIN_CPP_STATES_H + +namespace facebook { + namespace react { + class EngineViewState { + public: + EngineViewState() = default; + }; + } +} + +#endif \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/ets/BabylonPackage.ts b/react-native-harmony/harmony/rn_babylon/src/main/ets/BabylonPackage.ts new file mode 100644 index 000000000..d8a3fb6ba --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/ets/BabylonPackage.ts @@ -0,0 +1,46 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import type { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony/ts'; +import { RNPackage, UITurboModuleFactory } from '@rnoh/react-native-openharmony/ts'; +import { RNBabylonModule } from './RNBabylonModule'; +import { TM } from "./namespace/ts" + +class BabylonPackageTurboModulesFactory extends UITurboModuleFactory { + createTurboModule(name: string): UITurboModule | null { + if (name === TM.RNBabylonModule.NAME) { + return new RNBabylonModule(this.ctx); + } + return null; + } + + hasTurboModule(name: string): boolean { + return name === TM.RNBabylonModule.NAME; + } +} + +export class BabylonPackage extends RNPackage { + createTurboModulesFactory(ctx: UITurboModuleContext): UITurboModuleFactory { + return new BabylonPackageTurboModulesFactory(ctx); + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/ets/RNBabylonModule.ts b/react-native-harmony/harmony/rn_babylon/src/main/ets/RNBabylonModule.ts new file mode 100644 index 000000000..1c0d7dae0 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/ets/RNBabylonModule.ts @@ -0,0 +1,38 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts'; +import { TM } from "./namespace/ts" + +export class RNBabylonModule extends TurboModule implements TM.RNBabylonModule.Spec{ + + // 这里是arkts接口的具体实现文件,经过验证,纯jsi接口的项目,在arkts侧也需要构造一个对应的TurboModule。 + addListener(eventName: string) { + // this.logger.warn("DevSettings::addListener is not supported"); + } + + removeListeners(count: number) { + // this.logger.warn("DevSettings::removeListeners is not supported"); + } + +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/RNBabylonModule.ts b/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/RNBabylonModule.ts new file mode 100644 index 000000000..e98ddec93 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/RNBabylonModule.ts @@ -0,0 +1,32 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export namespace RNBabylonModule { + export const NAME = 'NativeRNBabylonModule' as const + + export interface Spec { + // 这里是定义arkts接口的文件,如果有需要,可在这里增加arkts接口 + addListener(eventName: string); + removeListeners(count: number); + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/ts.ts b/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/ts.ts new file mode 100644 index 000000000..3662eb680 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/ets/namespace/ts.ts @@ -0,0 +1,24 @@ +/** + * MIT License + * + * Copyright (C) 2025 Huawei Device Co., Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export * as TM from "./RNBabylonModule" \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/module.json5 b/react-native-harmony/harmony/rn_babylon/src/main/module.json5 new file mode 100644 index 000000000..fa137cf65 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/module.json5 @@ -0,0 +1,9 @@ +{ + "module": { + "name": "bl", + "type": "har", + "deviceTypes": [ + "default" + ], + } +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/color.json b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/color.json new file mode 100644 index 000000000..3c712962d --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/string.json b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/string.json new file mode 100644 index 000000000..14ac8a4a2 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "WebviewAbility_desc", + "value": "description" + }, + { + "name": "WebviewAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/base/media/icon.png b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/media/icon.png new file mode 100644 index 000000000..ce307a882 Binary files /dev/null and b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/media/icon.png differ diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/base/profile/main_pages.json b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/profile/main_pages.json new file mode 100644 index 000000000..1898d94f5 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/en_US/element/string.json b/react-native-harmony/harmony/rn_babylon/src/main/resources/en_US/element/string.json new file mode 100644 index 000000000..14ac8a4a2 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "WebviewAbility_desc", + "value": "description" + }, + { + "name": "WebviewAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/src/main/resources/zh_CN/element/string.json b/react-native-harmony/harmony/rn_babylon/src/main/resources/zh_CN/element/string.json new file mode 100644 index 000000000..948b6ca7c --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "WebviewAbility_desc", + "value": "description" + }, + { + "name": "WebviewAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/react-native-harmony/harmony/rn_babylon/ts.ts b/react-native-harmony/harmony/rn_babylon/ts.ts new file mode 100644 index 000000000..a31238ac0 --- /dev/null +++ b/react-native-harmony/harmony/rn_babylon/ts.ts @@ -0,0 +1,3 @@ +export * from "./src/main/ets/BabylonPackage" +export * from "./src/main/ets/RNBabylonModule" + diff --git a/react-native-harmony/package.json b/react-native-harmony/package.json new file mode 100644 index 000000000..d9aac3512 --- /dev/null +++ b/react-native-harmony/package.json @@ -0,0 +1,51 @@ +{ + "name": "@react-native-oh-tpl/babylonjs-react-native-harmony", + "title": "React Native Babylon for iOS and Android", + "version": "0.0.1-0.0.1", + "description": "Babylon Native integration into React Native", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "harmony": { + "alias": "@babylonjs/react-native-iosandroid" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/babylonjs/BabylonReactNative.git", + "baseUrl": "https://github.com/babylonjs/BabylonReactNative" + }, + "keywords": [ + "react-native" + ], + "author": { + "name": "Your Name", + "email": "yourname@email.com" + }, + "homepage": "https://github.com/BabylonJS/BabylonReactNative#readme", + "license": "MIT", + "licenseFilename": "LICENSE", + "readmeFilename": "README.md", + "dependencies": { + "base-64": "^0.1.0", + "semver": "^7.3.2" + }, + "peerDependencies": { + "@babylonjs/react-native": "*", + "react": "*", + "react-native": "*", + "react-native-permissions": ">=3.0.0" + }, + "devDependencies": { + "@rnw-scripts/eslint-config": "0.1.6", + "@rnw-scripts/ts-config": "0.1.0", + "eslint": "7.12.0", + "just-scripts": "^0.44.7", + "prettier": "1.19.1", + "typescript": "^4.3.5" + } +} diff --git a/react-native-harmony/react-native-babylon.podspec b/react-native-harmony/react-native-babylon.podspec new file mode 100644 index 000000000..b5a95ae6e --- /dev/null +++ b/react-native-harmony/react-native-babylon.podspec @@ -0,0 +1,50 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +# This Podspec is used for local development + +Pod::Spec.new do |s| + s.name = "react-native-babylon" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => "12.0" } + s.source = { :git => package["repository"]["url"], :tag => s.version } + + s.source_files = "ios/*.{h,m,mm}" + s.requires_arc = true + s.xcconfig = { 'USER_HEADER_SEARCH_PATHS' => '$(inherited) ${PODS_TARGET_SRCROOT}/shared ${PODS_TARGET_SRCROOT}/../react-native/shared' } + + s.vendored_frameworks = "ios/libs/*.xcframework" + + s.frameworks = "MetalKit", "ARKit" + + # install_modules_dependencies has been defined in RN 0.70 + # This check ensure that the library can work on older versions of RN + if defined?(install_modules_dependencies) + install_modules_dependencies(s) + else + s.dependency "React-Core" + + # Don't install the dependencies when we run `pod install` in the old architecture. + if new_arch_enabled then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-RCTFabric" + end + end +end + diff --git a/react-native/.gitattributes b/react-native/.gitattributes new file mode 100644 index 000000000..d42ff1835 --- /dev/null +++ b/react-native/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/react-native/BabylonModule.ts b/react-native/BabylonModule.ts new file mode 100644 index 000000000..0e06d68ac --- /dev/null +++ b/react-native/BabylonModule.ts @@ -0,0 +1,29 @@ +import NativeRNBabylonModule from './NativeRNBabylonModule'; + +declare const global: { + nativeCallSyncHook: any; +}; +const isRemoteDebuggingEnabled = !global.nativeCallSyncHook; + +// This legacy React Native module is created by Babylon React Native, and is only used to bootstrap the JSI object creation. +// This will likely be removed when the BabylonNative global object is eventually converted to a TurboModule. +// const BabylonModule: { +// initialize(): Promise; +// resetView(): Promise; +// } = NativeModules.BabylonModule; + +export async function ensureInitialized(): Promise { + if (isRemoteDebuggingEnabled) { + // When remote debugging is enabled, JavaScript runs on the debugging host machine, not on the device where the app is running. + // JSI (which Babylon Native uses heavily) can not work in this mode. In the future, this debugging mode will be phased out as it is incompatible with TurboModules for the same reason. + return false; + } else { + // This does the first stage of Babylon Native initialization, including creating the BabylonNative JSI object. + await NativeRNBabylonModule.initialize(); + return true; + } +} + +export async function reset(): Promise { + return NativeRNBabylonModule.resetView(); +} \ No newline at end of file diff --git a/react-native/EngineHook.ts b/react-native/EngineHook.ts new file mode 100644 index 000000000..c86fe2d18 --- /dev/null +++ b/react-native/EngineHook.ts @@ -0,0 +1,230 @@ +import { useEffect, useState } from 'react'; +import { Platform } from 'react-native'; +import { PERMISSIONS, check, request } from 'react-native-permissions'; +import { Engine, WebXRSessionManager, WebXRExperienceHelper, Color4, Tools, VideoTexture } from '@babylonjs/core'; +import { ReactNativeEngine } from './ReactNativeEngine'; +import { ensureInitialized } from './BabylonModule'; + +import * as base64 from 'base-64'; + +// These are errors that are normally thrown by WebXR's requestSession, so we should throw the same errors under similar circumstances so app code can be written the same for browser or native. +// https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/requestSession +// https://developer.mozilla.org/en-US/docs/Web/API/DOMException#Error_names +enum DOMError { + NotSupportedError = 9, + InvalidStateError = 11, + SecurityError = 18, +} + +class DOMException { + public constructor(private readonly error: DOMError) { } + get code(): number { return this.error; } + get name(): string { return DOMError[this.error]; } +} + +// Requests the camera permission and throws if the permission could not be granted +async function requestCameraPermissionAsync() : Promise { + const cameraPermission = Platform.select({ + android: PERMISSIONS.ANDROID.CAMERA, + ios: PERMISSIONS.IOS.CAMERA, + }); + + // Only Android, iOS and Windows are supported. + if (cameraPermission === undefined) { + throw new DOMException(DOMError.NotSupportedError); + } + + // If the permission has not been granted yet, but also not been blocked, then request permission. + let permissionStatus = await check(cameraPermission); + if (permissionStatus == "denied") + { + permissionStatus = await request(cameraPermission); + } + + // If the permission has still not been granted, then throw an appropriate exception, otherwise continue with the actual XR session initialization. + switch(permissionStatus) { + case "unavailable": + throw new DOMException(DOMError.NotSupportedError); + case "denied": + case "blocked": + throw new DOMException(DOMError.SecurityError); + case "granted": + return; + } +} + +// Override the WebXRSessionManager.initializeSessionAsync to insert a camera permissions request. It would be cleaner to do this directly in the native XR implementation, but there are a couple problems with that: +// 1. React Native does not provide a way to hook into the permissions request result (at least on Android). +// 2. If it is done on the native side, then we need one implementation per platform. +{ + const originalInitializeSessionAsync = WebXRSessionManager.prototype.initializeSessionAsync; + WebXRSessionManager.prototype.initializeSessionAsync = async function (...args: Parameters): ReturnType { + if (Platform.OS === "windows") + { + // Launching into immersive mode on Windows HMDs doesn't require a runtime permission check. + // The Spatial Perception capability should be enabled in the project's Package.appxmanifest. + return originalInitializeSessionAsync.apply(this, args); + } + + // await requestCameraPermissionAsync(); + + return originalInitializeSessionAsync.apply(this, args); + } +} + +ensureInitialized().then(() => { + // Override the navigator.mediaDevices.getUserMedia to insert a camera permissions request. It would be cleaner to do this directly in the NativeCamera implementation, but there are a couple problems with that: + // 1. React Native does not provide a way to hook into the permissions request result (at least on Android). + // 2. If it is done on the native side, then we need one implementation per platform. + { + const originalGetUserMedia = navigator.mediaDevices.getUserMedia; + navigator.mediaDevices.getUserMedia = async function (...args: Parameters): ReturnType { + // await requestCameraPermissionAsync(); + + return originalGetUserMedia.apply(this, args); + } + } +}); + +if (Platform.OS === "android" || Platform.OS === "ios" || Platform.OS as string === 'harmony') { + const originalEnterXRAsync: (...args: any[]) => Promise = WebXRExperienceHelper.prototype.enterXRAsync; + WebXRExperienceHelper.prototype.enterXRAsync = async function (...args: any[]): Promise { + // TODO: https://github.com/BabylonJS/BabylonNative/issues/649 + // Android/iOS require manually clearing the default frame buffer to prevent garbage from being rendered for a few frames during the XR transition + const sessionManager = await originalEnterXRAsync.apply(this, args); + const scene = sessionManager.scene; + const beforeRenderObserver = scene.onBeforeRenderObservable.add(() => { + scene.getEngine().unBindFramebuffer(undefined!); + scene.getEngine().clear(scene.clearColor, true, false); + }); + sessionManager.onXRSessionEnded.add(() => { + scene.onBeforeRenderObservable.remove(beforeRenderObserver); + }); + return sessionManager; + }; +} else if (Platform.OS === "windows") { + const originalEnterXRAsync: (...args: any[]) => Promise = WebXRExperienceHelper.prototype.enterXRAsync; + WebXRExperienceHelper.prototype.enterXRAsync = async function (...args: any[]): Promise { + // TODO: https://github.com/BabylonJS/BabylonNative/issues/577 + // Windows HMDs require different rendering behaviors than default xr rendering for mobile devices + const sessionManager = await originalEnterXRAsync.apply(this, args); + sessionManager.scene.clearColor = new Color4(0, 0, 0, 0); + sessionManager.scene.autoClear = true; + return sessionManager; + }; +} + +// Babylon Native includes a native atob polyfill, but it relies JSI to deal with the strings, and JSI has a bug where it assumes strings are null terminated, and a base 64 string can contain one of these. +// So for now, provide a JavaScript based atob polyfill. +declare const global: any; +global.atob = base64.decode; + +// Polyfill console.time and console.timeEnd if needed (as of React Native 0.64 these are not implemented). +if (!console.time) { + const consoleTimes = new Map(); + + console.time = (label = "default"): void => { + consoleTimes.set(label, performance.now()); + }; + + console.timeEnd = (label = "default"): void => { + const end = performance.now(); + const start = consoleTimes.get(label); + if (!!start) { + consoleTimes.delete(label); + console.log(`${label}: ${end - start} ms`); + } + } +} + +// Hook Tools performance counter functions to forward to NativeTracing. +// Ideally this should be hooked more directly in Babylon.js so it works with Babylon Native as well, but we need to determine a pattern for augmenting Babylon.js with Babylon Native specific JS logic. +declare var _native: { + enablePerformanceLogging(): void, + disablePerformanceLogging(): void, + startPerformanceCounter(counter: string): unknown, + endPerformanceCounter(counter: unknown): void, +}; + +{ + const setPerformanceLogLevel: ((level: number) => void) | undefined = Object.getOwnPropertyDescriptor(Tools, "PerformanceLogLevel")?.set; + if (!setPerformanceLogLevel) { + console.warn(`NativeTracing was not hooked into Babylon.js performance logging because the Tools.PerformanceLogLevel property does not exist.`); + } else { + // Keep a map of trace region opaque pointers since Tools.EndPerformanceCounter just takes a counter name as an argument. + const traceRegions = new Map(); + let currentLevel = Tools.PerformanceNoneLogLevel; + Object.defineProperty(Tools, "PerformanceLogLevel", { + set: (level: number) => { + // No-op if the log level isn't changing, otherwise we can end up with multiple wrapper layers repeating the same work. + if (level !== currentLevel) { + currentLevel = level; + + // Invoke the original PerformanceLevel setter. + setPerformanceLogLevel(currentLevel); + + if (currentLevel === Tools.PerformanceNoneLogLevel) { + _native.disablePerformanceLogging(); + } else { + _native.enablePerformanceLogging(); + + // When Tools.PerformanceLogLevel is set, it assigns the Tools.StartPerformanceCounter and Tools.EndPerformanceCounter functions, so we need to assign + // these functions again in order to wrap them. + + const originalStartPerformanceCounter = Tools.StartPerformanceCounter; + Tools.StartPerformanceCounter = (counterName: string, condition = true) => { + // Call into native before so the time it takes is not captured in the JS perf counter interval. + if (condition) { + if (traceRegions.has(counterName)) { + console.warn(`Performance counter '${counterName}' already exists.`); + } else { + traceRegions.set(counterName, _native.startPerformanceCounter(counterName)); + } + } + + originalStartPerformanceCounter(counterName, condition); + }; + + const originalEndPerformanceCounter = Tools.EndPerformanceCounter; + Tools.EndPerformanceCounter = (counterName: string, condition = true) => { + originalEndPerformanceCounter(counterName, condition); + + // Call into native after so the time it takes is not captured in the JS perf counter interval. + if (condition) { + const traceRegion = traceRegions.get(counterName); + if (traceRegion) { + _native.endPerformanceCounter(traceRegion); + traceRegions.delete(counterName); + } else { + console.warn(`Performance counter '${counterName}' does not exist.`); + } + } + } + } + } + }, + }); + } +} + +export function useEngine(): Engine | undefined { + const [engine, setEngine] = useState(); + + useEffect(() => { + const abortController = new AbortController(); + let engine: ReactNativeEngine | undefined = undefined; + + (async () => { + setEngine(engine = await ReactNativeEngine.tryCreateAsync(abortController.signal) ?? undefined); + })(); + + return () => { + abortController.abort(); + // NOTE: Do not use setEngine with a callback to dispose the engine instance as that callback does not get called during component unmount when compiled in release. + engine?.dispose(); + setEngine(undefined); + }; + }, []); + + return engine; +} diff --git a/react-native/EngineView.tsx b/react-native/EngineView.tsx new file mode 100644 index 000000000..749dddd05 --- /dev/null +++ b/react-native/EngineView.tsx @@ -0,0 +1,144 @@ +import React, { Component, FunctionComponent, SyntheticEvent, useCallback, useEffect, useState, useRef, useMemo } from 'react'; +import { ViewProps, View, Text, findNodeHandle, UIManager } from 'react-native'; +import { Camera, SceneInstrumentation } from '@babylonjs/core'; +import { ReactNativeEngine } from './ReactNativeEngine'; +import { useModuleInitializer, useRenderLoop } from './NativeEngineHook'; +import NativeEngineView, { NativeEngineViewProps, Commands as EngineViewCommands } from './NativeEngineView'; + +export interface EngineViewProps extends ViewProps { + camera?: Camera; + displayFrameRate?: boolean; + isTransparent?: boolean; + androidView?: "TextureView" | "SurfaceView" | "SurfaceViewZTopMost" | "SurfaceViewZMediaOverlay"; + antiAliasing?: 0 | 1 | 2 | 4 | 8 | 16; + onInitialized?: (view: EngineViewCallbacks) => void; +} + +export interface EngineViewCallbacks { + takeSnapshot: () => Promise; +} + +interface SceneStats { + frameRate: number, + frameTime: number, +} + +export const EngineView: FunctionComponent = (props: EngineViewProps) => { + //const [fps, setFps] = useState(); + const [sceneStats, setSceneStats] = useState(); + const engineViewRef = useRef>(null); + const snapshotPromise = useRef<{ promise: Promise, resolve: (data: string) => void }>(); + const isTransparent = props.isTransparent ?? false; + const antiAliasing = props.antiAliasing ?? 0; + const androidView = props.androidView ?? ""; + + const initialized = useModuleInitializer(); + + const engine = useMemo(() => { + return props.camera?.getScene().getEngine() as ReactNativeEngine; + }, [props.camera]); + + const renderLoop = useCallback(() => { + for (let scene of engine.scenes) { + scene.render(); + } + }, [engine]); + + useRenderLoop(engine, renderLoop); + + useEffect(() => { + if (props.camera && (props.displayFrameRate ?? __DEV__)) { + const scene = props.camera.getScene(); + const engine = scene.getEngine() as ReactNativeEngine; + + if (!engine.isDisposed) { + setSceneStats({frameRate: 0, frameTime: 0}); + + const sceneInstrumentation = new SceneInstrumentation(scene); + sceneInstrumentation.captureFrameTime = true; + + const timerHandle = setInterval(() => { + setSceneStats({frameRate: engine.getFps(), frameTime: sceneInstrumentation.frameTimeCounter.lastSecAverage}); + }, 1000); + + return () => { + clearInterval(timerHandle); + setSceneStats(undefined); + sceneInstrumentation.dispose(); + }; + } + } + + return undefined; + }, [props.camera, props.displayFrameRate]); + + // Call onInitialized if provided, and include the callback for takeSnapshot. + useEffect(() => { + if (props.onInitialized) { + props.onInitialized({ + takeSnapshot: (): Promise => { + if (!snapshotPromise.current) { + let resolveFunction: ((data: string) => void) | undefined; + const promise = new Promise((resolutionFunc) => { + resolveFunction = resolutionFunc; + }); + + // Resolution functions should always be initialized. + if (resolveFunction) { + snapshotPromise.current = { promise: promise, resolve: resolveFunction }; + } + else { + throw new Error("Resolution functions not initialized after snapshot promise creation."); + } + + if (engineViewRef.current) { + EngineViewCommands.takeSnapshot(engineViewRef.current); + } else { + throw new Error("EngineView reference not initialized"); + } + } + + return snapshotPromise.current.promise; + } + }); + } + }, [props.onInitialized]); + + // Handle snapshot data returned. + const snapshotDataReturnedHandler = useCallback((event:any) => { + // The nativeEvent is a DOMEvent which doesn't have a typescript definition. Cast it to an Event object with a data property. + const { data } = event.nativeEvent as Event & { data: string }; + if (snapshotPromise.current) { + snapshotPromise.current.resolve(data); + snapshotPromise.current = undefined; + } + }, []); + + if (initialized !== false) { + return ( + + { initialized && } + { sceneStats !== undefined && + + FPS: {sceneStats.frameRate.toFixed(0)} + {/* Frame time seems wonky... it goes down when manipulating the scaling slider in the Playground app. Investigate this before showing it so we don't show data that can't be trusted. */} + {/* | + Frame Time: {sceneStats.frameTime.toFixed(1)}ms */} + + } + + ); + } else { + const message = "Could not initialize Babylon Native."; + if (!__DEV__) { + throw new Error(message); + } + + return ( + + {message} + React Native remote debugging does not work with Babylon Native. + + ); + } +} diff --git a/react-native/FontFace.ts b/react-native/FontFace.ts new file mode 100644 index 000000000..e82ad4380 --- /dev/null +++ b/react-native/FontFace.ts @@ -0,0 +1,39 @@ +import { Tools } from '@babylonjs/core'; + +// Declare _native to get access to NativeCanvas. +declare var _native: any; + +/** + * Partial Polyfill for FontFace Web API to wrap the NativeCanvas object. + */ +export class FontFace { + private source: string | ArrayBuffer | undefined; + private _status: "unloaded" | "loading" | "loaded" | "error" = "unloaded"; + public get status(): "unloaded" | "loading" | "loaded" | "error" { + return this._status; + } + + public get loaded(): boolean { + return this._status === "loaded"; + } + + public constructor(public readonly family: string, source: string | ArrayBuffer) { + this.source = source; + } + + public async load(): Promise { + try { + this._status = "loading"; + if (typeof this.source === 'string') { + this.source = await Tools.LoadFileAsync(this.source); + } + + await _native.Canvas.loadTTFAsync(this.family, this.source); + this.source = undefined; + this._status = "loaded" + } catch (ex) { + console.error("Error encountered when loading font: " + ex); + this._status = "error"; + } + } +} \ No newline at end of file diff --git a/react-native/NativeCapture.ts b/react-native/NativeCapture.ts new file mode 100644 index 000000000..209f8515b --- /dev/null +++ b/react-native/NativeCapture.ts @@ -0,0 +1,32 @@ +import { Camera } from '@babylonjs/core'; + +export type CapturedFrame = { + width: number; + height: number; + format: "RGBA8" | "BGRA8" | undefined; + yFlip: boolean; + data: ArrayBuffer; +}; + +export type CaptureCallback = (capture: CapturedFrame) => void; + +declare class NativeCapture { + public constructor(frameBuffer: unknown); + public addCallback(onCaptureCallback: CaptureCallback): void; + public dispose(): void; +}; + +export class CaptureSession { + private readonly nativeCapture: NativeCapture; + + public constructor(camera: Camera | undefined, onCaptureCallback: CaptureCallback) { + console.warn(`CaptureSession is experimental and likely to change significantly.`); + // HACK: There is no exposed way to access the frame buffer from render target texture + this.nativeCapture = new NativeCapture((camera?.outputRenderTarget?.renderTarget as any)?._framebuffer); + this.nativeCapture.addCallback(onCaptureCallback); + } + + public dispose(): void { + this.nativeCapture.dispose(); + } +} \ No newline at end of file diff --git a/react-native/NativeEngineHook.ts b/react-native/NativeEngineHook.ts new file mode 100644 index 000000000..d8263540f --- /dev/null +++ b/react-native/NativeEngineHook.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { AppState, AppStateStatus } from 'react-native'; +import { ensureInitialized } from './BabylonModule'; +import { ReactNativeEngine } from './ReactNativeEngine'; + +import './VersionValidation'; + +export function useModuleInitializer(): boolean | undefined { + const [initialized, setInitialized] = useState(); + + useEffect(() => { + const abortController = new AbortController(); + (async () => { + const isInitialized = await ensureInitialized(); + + if (!abortController.signal.aborted) { + setInitialized(isInitialized); + } + })(); + + return () => { + abortController.abort(); + } + }, []); + + return initialized; +} + +function useAppState(): string { + const [appState, setAppState] = useState(AppState.currentState); + + useEffect(() => { + const onAppStateChanged = (appState: AppStateStatus) => { + setAppState(appState); + }; + + const appStateListener = AppState.addEventListener("change", onAppStateChanged); + + // Asserting the type to prevent TS type errors on older RN versions + const removeListener = appStateListener?.["remove"] as undefined | Function; + + return () => { + if (!!removeListener) { + removeListener(); + } else { + appStateListener.remove(); + } + }; + }, []); + + return appState; +} + +export function useRenderLoop(engine: ReactNativeEngine | undefined, renderCallback: () => void): void { + const appState = useAppState(); + + useEffect(() => { + if (engine && appState === "active") { + if (!engine.isDisposed) { + engine.runRenderLoop(renderCallback); + + return () => { + if (!engine.isDisposed) { + engine.stopRenderLoop(); + } + }; + } + } + + return undefined; + }, [appState, engine]); +} diff --git a/react-native/NativeEngineView.tsx b/react-native/NativeEngineView.tsx new file mode 100644 index 000000000..adb517d37 --- /dev/null +++ b/react-native/NativeEngineView.tsx @@ -0,0 +1,29 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import codegenNativeCommands from "react-native/Libraries/Utilities/codegenNativeCommands"; +import type { ViewProps, HostComponent } from 'react-native'; +import { Int32,DirectEventHandler,WithDefault } from "react-native/Libraries/Types/CodegenTypes"; + +export type SnapshotEventData = Readonly<{ + data?: string; + }> + +export interface NativeEngineViewProps extends ViewProps { + isTransparent?: WithDefault; + antiAliasing?: Int32; + androidView?: string; + onSnapshotDataReturned?: DirectEventHandler; +} + +type NativeType = HostComponent; + +interface NativeCommands { + takeSnapshot: (viewRef: React.ElementRef) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ["takeSnapshot"], +}); + +export default codegenNativeComponent( + "NativeEngineView" + ) as HostComponent; diff --git a/react-native/NativeRNBabylonModule.ts b/react-native/NativeRNBabylonModule.ts new file mode 100644 index 000000000..2dc5af4c7 --- /dev/null +++ b/react-native/NativeRNBabylonModule.ts @@ -0,0 +1,9 @@ +import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + initialize(): Promise; + resetView(): Promise; +} + +export default TurboModuleRegistry.getEnforcing('NativeRNBabylonModule'); \ No newline at end of file diff --git a/react-native/ReactNativeEngine.ts b/react-native/ReactNativeEngine.ts new file mode 100644 index 000000000..04d9f3268 --- /dev/null +++ b/react-native/ReactNativeEngine.ts @@ -0,0 +1,45 @@ +import { ensureInitialized, reset } from './BabylonModule'; +import { NativeEngine } from '@babylonjs/core'; + +// This JSI-based global object is owned by Babylon React Native. +// This will likely be converted to a TurboModule when they are fully supported. +declare const BabylonNative: { + readonly initializationPromise: Promise; + reset: () => void; + resetInitializationPromise: () => void; +}; + +export class ReactNativeEngine extends NativeEngine { + public _isDisposed = false; + + public static async tryCreateAsync(abortSignal: AbortSignal): Promise { + if (!await ensureInitialized() || abortSignal.aborted) { + return null; + } + + // This waits Graphics/NativeEngine to be created. + await BabylonNative.initializationPromise; + + // Check for cancellation. + if (abortSignal.aborted) { + return null; + } + + return new ReactNativeEngine(); + } + + public get isDisposed() { + return this._isDisposed; + } + + public dispose(): void { + if (!this.isDisposed) { + super.dispose(); + + BabylonNative.resetInitializationPromise(); + reset(); + + this._isDisposed = true; + } + } +} \ No newline at end of file diff --git a/react-native/VersionValidation.ts b/react-native/VersionValidation.ts new file mode 100644 index 000000000..b8d4ec491 --- /dev/null +++ b/react-native/VersionValidation.ts @@ -0,0 +1,13 @@ +import { Engine } from '@babylonjs/core'; +import { satisfies, minVersion } from 'semver' +import packageDefinition from './package.json'; + +if (__DEV__) { + const babylonJSPackageName = "@babylonjs/core"; + const requiredVersion = packageDefinition.peerDependencies[babylonJSPackageName]; + const minRequiredVersion = minVersion(requiredVersion); + const currentVersion = Engine.Version; + if (!satisfies(currentVersion, requiredVersion)) { + console.error(`${packageDefinition.name}@${packageDefinition.version} requires ${babylonJSPackageName}@${requiredVersion} but version ${currentVersion} is currently installed. Run 'npm install ${babylonJSPackageName}@${minRequiredVersion}' in your app's root directory to upgrade.`); + } +} \ No newline at end of file diff --git a/react-native/index.ts b/react-native/index.ts new file mode 100644 index 000000000..dd2811940 --- /dev/null +++ b/react-native/index.ts @@ -0,0 +1,4 @@ +export * from './EngineView'; +export * from './EngineHook'; +export * from './NativeCapture'; +export * from './FontFace'; diff --git a/react-native/jest.config.js b/react-native/jest.config.js new file mode 100644 index 000000000..e08dd7f88 --- /dev/null +++ b/react-native/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'react-native', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; \ No newline at end of file diff --git a/react-native/package.json b/react-native/package.json new file mode 100644 index 000000000..e65cbb659 --- /dev/null +++ b/react-native/package.json @@ -0,0 +1,66 @@ +{ + "name": "@react-native-oh-tpl/babylonjs-react-native", + "title": "React Native Babylon", + "version": "0.0.1-0.0.1", + "harmony": { + "alias": "@babylonjs/react-native", + "codegenConfig": { + "specPaths": [ + "./NativeEngineView.tsx" + ] + } + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "description": "Babylon Native integration into React Native", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/babylonjs/BabylonReactNative.git", + "baseUrl": "https://github.com/babylonjs/BabylonReactNative" + }, + "keywords": [ + "react-native" + ], + "author": { + "name": "Your Name", + "email": "yourname@email.com" + }, + "homepage": "https://github.com/BabylonJS/BabylonReactNative#readme", + "license": "MIT", + "licenseFilename": "LICENSE", + "readmeFilename": "README.md", + "dependencies": { + "base-64": "^0.1.0", + "semver": "^7.3.2" + }, + "peerDependencies": { + "@babylonjs/core": ">=5.53.1", + "react": "*", + "react-native": "*", + "react-native-permissions": ">=3.0.0" + }, + "devDependencies": { + "@babel/core": "^7.8.4", + "@babel/runtime": "^7.8.4", + "@babylonjs/core": ">=5.53.1", + "@rnw-scripts/eslint-config": "0.1.6", + "@rnw-scripts/ts-config": "0.1.0", + "@types/base-64": "^0.1.3", + "@types/jest": "^25.2.1", + "react-native": "0.73.5", + "@types/react": "^18.3.0", + "react-native-permissions": "^4.1.5", + "@types/react-test-renderer": "^16.9.2", + "@types/semver": "^7.3.4", + "eslint": "7.12.0", + "just-scripts": "^0.44.7", + "prettier": "1.19.1", + "typescript": "^4.3.5" + } +} diff --git a/react-native/shared/BabylonNative.cpp b/react-native/shared/BabylonNative.cpp new file mode 100644 index 000000000..c7b36ded2 --- /dev/null +++ b/react-native/shared/BabylonNative.cpp @@ -0,0 +1,415 @@ +#include "BabylonNative.h" + +#include +#include +#ifndef BASEKIT_BUILD +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace BabylonNative +{ + using namespace Babylon::Plugins; + using namespace facebook; + + namespace + { + Dispatcher g_inlineDispatcher{ [](const std::function& func) { func(); } }; + std::optional g_graphicsDevice{}; + std::optional g_update{}; + std::unique_ptr g_nativeCanvas{}; + } + + const uint32_t LEFT_MOUSE_BUTTON_ID{ NativeInput::LEFT_MOUSE_BUTTON_ID }; + const uint32_t MIDDLE_MOUSE_BUTTON_ID{ NativeInput::MIDDLE_MOUSE_BUTTON_ID }; + const uint32_t RIGHT_MOUSE_BUTTON_ID{ NativeInput::RIGHT_MOUSE_BUTTON_ID }; + + class ReactNativeModule : public jsi::HostObject + { + public: + ReactNativeModule(jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher) + : m_env{ Napi::Attach(jsiRuntime) } + , m_jsDispatcher{ std::move(jsDispatcher) } + , m_isRunning{ std::make_shared(true) } + , m_isXRActive{ std::make_shared(false) } + { + // Initialize a JS promise that will be returned by whenInitialized, and completed when NativeEngine is initialized. + CreateInitPromise(); + + // Initialize Babylon Native core components + Babylon::JsRuntime::CreateForJavaScript(m_env, Babylon::CreateJsRuntimeDispatcher(m_env, jsiRuntime, m_jsDispatcher, m_isRunning)); + + // Initialize Babylon Native plugins +#ifndef BASEKIT_BUILD + m_nativeXr.emplace(Babylon::Plugins::NativeXr::Initialize(m_env)); + m_nativeXr->SetSessionStateChangedCallback([isXRActive{ m_isXRActive }](bool isSessionActive) { *isXRActive = isSessionActive; }); + Babylon::Plugins::NativeCamera::Initialize(m_env); +#endif + Babylon::Plugins::NativeCapture::Initialize(m_env); + m_nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(m_env); + Babylon::Plugins::NativeOptimizations::Initialize(m_env); + Babylon::Plugins::NativeTracing::Initialize(m_env); + + // Initialize Babylon Native polyfills + Babylon::Polyfills::Window::Initialize(m_env); + + // NOTE: React Native's XMLHttpRequest is slow and allocates a lot of memory. This does not override + // React Native's implementation, but rather adds a second one scoped to Babylon and used by WebRequest.ts. + Babylon::Polyfills::XMLHttpRequest::Initialize(m_env); + + // Initialize Canvas polyfill for text support + g_nativeCanvas = std::make_unique(Babylon::Polyfills::Canvas::Initialize(m_env)); + } + + ~ReactNativeModule() override + { + *m_isRunning = false; + Napi::Detach(m_env); + } + + void UpdateView(WindowType window, size_t width, size_t height) + { + m_graphicsConfig.Window = window; + m_graphicsConfig.Width = width; + m_graphicsConfig.Height = height; + UpdateGraphicsConfiguration(); + } + + void UpdateGraphicsConfiguration() + { + if (!g_graphicsDevice) + { + g_graphicsDevice.emplace(m_graphicsConfig); + g_update.emplace(g_graphicsDevice->GetUpdate("update")); + } + else + { + g_graphicsDevice->UpdateWindow(m_graphicsConfig.Window); + g_graphicsDevice->UpdateSize(m_graphicsConfig.Width, m_graphicsConfig.Height); + } + g_graphicsDevice->UpdateMSAA(mMSAAValue); + g_graphicsDevice->UpdateAlphaPremultiplied(mAlphaPremultiplied); + + g_graphicsDevice->EnableRendering(); + + std::call_once(m_isGraphicsInitialized, [this]() + { + m_jsDispatcher([this]() + { + g_graphicsDevice->AddToJavaScript(m_env); + Babylon::Plugins::NativeEngine::Initialize(m_env); + }); + }); + + if (!m_isRenderingEnabled) + { + m_jsDispatcher([this]() + { + m_resolveInitPromise(); + }); + } + m_isRenderingEnabled = true; + m_newEngine = false; + } + + void UpdateMSAA(uint8_t value) + { + mMSAAValue = value; + if (g_graphicsDevice) + { + g_graphicsDevice->UpdateMSAA(value); + } + } + + void UpdateAlphaPremultiplied(bool enabled) + { + mAlphaPremultiplied = enabled; + if (g_graphicsDevice) + { + g_graphicsDevice->UpdateAlphaPremultiplied(enabled); + } + } + + void RenderView() + { + if (m_newEngine) + { + UpdateGraphicsConfiguration(); + } + // If rendering has not been explicitly enabled, or has been explicitly disabled, then don't try to render. + // Otherwise rendering can be implicitly enabled, which may not be desirable (e.g. after the engine is disposed). + if (g_graphicsDevice && m_isRenderingEnabled) + { + g_graphicsDevice->StartRenderingCurrentFrame(); + g_update->Start(); + g_update->Finish(); + g_graphicsDevice->FinishRenderingCurrentFrame(); + } + } + + void Initialize() + { + m_newEngine = true; + } + + void ResetView() + { + if (g_graphicsDevice) + { + g_nativeCanvas->FlushGraphicResources(); + g_graphicsDevice->DisableRendering(); + } + + m_isRenderingEnabled = false; + } + + void SetMouseButtonState(uint32_t buttonId, bool isDown, int32_t x, int32_t y) + { + if (isDown) + { + m_nativeInput->MouseDown(buttonId, x, y); + } + else + { + m_nativeInput->MouseUp(buttonId, x, y); + } + } + + void SetMousePosition(int32_t x, int32_t y) + { + m_nativeInput->MouseMove(x, y); + } + + void SetTouchButtonState(uint32_t pointerId, bool isDown, int32_t x, int32_t y) + { + if (isDown) + { + m_nativeInput->TouchDown(pointerId, x, y); + } + else + { + m_nativeInput->TouchUp(pointerId, x, y); + } + } + + void SetTouchPosition(uint32_t pointerId, int32_t x, int32_t y) + { + m_nativeInput->TouchMove(pointerId, x, y); + } + + bool IsXRActive() + { + return *m_isXRActive; + } + +#if defined(__APPLE__) || defined(ANDROID) || defined(__OHOS__) + void UpdateXRView(WindowType window) + { +#ifndef BASEKIT_BUILD + m_nativeXr->UpdateWindow(window); +#endif + } +#endif + + jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& prop) override + { + const auto propName{ prop.utf8(runtime) }; + + if (propName == "initializationPromise") + { + return { runtime, m_initPromise }; + } + else if (propName == "resetInitializationPromise") + { + return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forAscii(runtime, "executor"), 0, [this](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t) -> jsi::Value + { + CreateInitPromise(); + return {}; + }); + } + + return {}; + } + private: + void CreateInitPromise() + { + jsi::Runtime& jsiRuntime{static_cast(m_env)->rt}; + m_initPromise = jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "Promise").callAsConstructor + ( + jsiRuntime, + jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "executor"), 0, [this](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t) -> jsi::Value + { + m_resolveInitPromise = [&rt, resolve{ std::make_shared(rt, args[0]) }]() + { + resolve->asObject(rt).asFunction(rt).call(rt); + }; + return {}; + }) + ); + } + + jsi::Value m_initPromise{}; + std::function m_resolveInitPromise{}; + + Napi::Env m_env; + Dispatcher m_jsDispatcher{}; + + std::shared_ptr m_isRunning{}; + bool m_isRenderingEnabled{}; + std::once_flag m_isGraphicsInitialized{}; + Babylon::Plugins::NativeInput* m_nativeInput{}; +#ifndef BASEKIT_BUILD + std::optional m_nativeXr{}; +#endif + Babylon::Graphics::Configuration m_graphicsConfig{}; + + std::shared_ptr m_isXRActive{}; + uint8_t mMSAAValue{}; + bool mAlphaPremultiplied{}; + bool m_newEngine{}; + }; + + namespace + { + constexpr auto JS_INSTANCE_NAME{ "BabylonNative" }; + std::weak_ptr g_nativeModule{}; + } + + void Initialize(facebook::jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher) + { + if (!jsiRuntime.global().hasProperty(jsiRuntime, JS_INSTANCE_NAME)) + { + auto nativeModule{ std::make_shared(jsiRuntime, jsDispatcher) }; + jsiRuntime.global().setProperty(jsiRuntime, JS_INSTANCE_NAME, jsi::Object::createFromHostObject(jsiRuntime, nativeModule)); + g_nativeModule = nativeModule; + } + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->Initialize(); + } + } + + void Deinitialize() + { + // Prevent further interactions with the native module from the native interface. + g_nativeModule.reset(); + } + + void UpdateView(WindowType window, size_t width, size_t height) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->UpdateView(window, width, height); + } + else + { + throw std::runtime_error{ "UpdateView must not be called before Initialize." }; + } + } + + void UpdateMSAA(uint8_t value) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->UpdateMSAA(value); + } + else + { + throw std::runtime_error{ "UpdateMSAA must not be called before Initialize." }; + } + } + + void UpdateAlphaPremultiplied(bool enabled) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->UpdateAlphaPremultiplied(enabled); + } + else + { + throw std::runtime_error{ "UpdateAlphaPremultiplied must not be called before Initialize." }; + } + } + + void RenderView() + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->RenderView(); + } + } + + void ResetView() + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->ResetView(); + } + else + { + throw std::runtime_error{ "ResetView must not be called before Initialize." }; + } + } + + void SetMouseButtonState(uint32_t buttonId, bool isDown, int32_t x, int32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetMouseButtonState(buttonId, isDown, x, y); + } + } + + void SetMousePosition(int32_t x, int32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetMousePosition(x, y); + } + } + + void SetTouchButtonState(uint32_t pointerId, bool isDown, int32_t x, int32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetTouchButtonState(pointerId, isDown, x, y); + } + } + + void SetTouchPosition(uint32_t pointerId, int32_t x, int32_t y) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->SetTouchPosition(pointerId, x, y); + } + } + + bool IsXRActive() + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + return nativeModule->IsXRActive(); + } + + return false; + } + +#if defined(__APPLE__) || defined(ANDROID) || defined(__OHOS__) + void UpdateXRView(WindowType window) + { + if (auto nativeModule{ g_nativeModule.lock() }) + { + nativeModule->UpdateXRView(window); + } + } +#endif +} diff --git a/react-native/shared/BabylonNative.h b/react-native/shared/BabylonNative.h new file mode 100644 index 000000000..3e517a421 --- /dev/null +++ b/react-native/shared/BabylonNative.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#if defined(__APPLE__) +#include +#elif defined(ANDROID) +#include +#elif defined(__OHOS__) +#include +#elif WINAPI_FAMILY == WINAPI_FAMILY_APP +#include +#endif + +namespace BabylonNative +{ + #if defined(__APPLE__) + using WindowType = MTKView*; + #elif defined(ANDROID) + using WindowType = ANativeWindow*; + #elif defined(__OHOS__) + using WindowType = OHNativeWindow*; + #elif WINAPI_FAMILY == WINAPI_FAMILY_APP + using WindowType = winrt::Windows::UI::Xaml::Controls::SwapChainPanel; + #else + #error Unsupported platform + #endif + + using Dispatcher = std::function)>; + + void Initialize(facebook::jsi::Runtime& jsiRuntime, Dispatcher jsDispatcher); + void Deinitialize(); + + void UpdateView(WindowType window, size_t width, size_t height); + void UpdateMSAA(uint8_t value); + void UpdateAlphaPremultiplied(bool enabled); + + void RenderView(); + void ResetView(); + void SetMouseButtonState(uint32_t buttonId, bool isDown, int32_t x, int32_t y); + void SetMousePosition(int32_t x, int32_t y); + void SetTouchButtonState(uint32_t pointerId, bool isDown, int32_t x, int32_t y); + void SetTouchPosition(uint32_t pointerId, int32_t x, int32_t y); + + bool IsXRActive(); + +#if defined(__APPLE__) || defined(ANDROID) || defined(__OHOS__) + void UpdateXRView(WindowType window); +#endif + + extern const uint32_t LEFT_MOUSE_BUTTON_ID; + extern const uint32_t MIDDLE_MOUSE_BUTTON_ID; + extern const uint32_t RIGHT_MOUSE_BUTTON_ID; +} diff --git a/react-native/shared/CMakeLists.txt b/react-native/shared/CMakeLists.txt new file mode 100644 index 000000000..1ae60c55a --- /dev/null +++ b/react-native/shared/CMakeLists.txt @@ -0,0 +1,22 @@ +include(FetchContent) + +FetchContent_Declare(babylonnative + GIT_REPOSITORY https://gitee.com/louis-damon/BabylonNative.git + GIT_TAG 6a2069e0e705667463b4e7e9e41ab226ff1eb62d) + +set(SHARED_INCLUDES + "${CMAKE_CURRENT_LIST_DIR}") + +set(SHARED_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/DispatchFunction.h" + "${CMAKE_CURRENT_LIST_DIR}/BabylonNative.h" + "${CMAKE_CURRENT_LIST_DIR}/BabylonNative.cpp") + +if (${BASEKIT_BUILD}) + set(BABYLON_NATIVE_PLUGIN_NATIVEXR OFF CACHE BOOL "") + set(BABYLON_NATIVE_PLUGIN_NATIVECAMERA OFF CACHE BOOL "") +endif() +FetchContent_GetProperties(babylonnative) +if(NOT babylonnative_POPULATED) + FetchContent_Populate(babylonnative) +endif() \ No newline at end of file diff --git a/react-native/shared/DispatchFunction.h b/react-native/shared/DispatchFunction.h new file mode 100644 index 000000000..c1655f2a7 --- /dev/null +++ b/react-native/shared/DispatchFunction.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include + +namespace Babylon +{ + using namespace facebook; + + // Creates a JsRuntime::DispatchFunctionT that integrates with the React Native execution environment. + inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, BabylonNative::Dispatcher dispatcher, const std::shared_ptr isRunning) + { + return [env, &jsiRuntime, dispatcher{ std::move(dispatcher) }, isRunning{ std::move(isRunning) }](std::function func) + { + // Ideally we would just use CallInvoker::invokeAsync directly, but currently it does not seem to integrate well with the React Native logbox. + // To work around this, we wrap all functions in a try/catch, and when there is an exception, we do the following: + // 1. Call the JavaScript setImmediate function. + // 2. Have the setImmediate callback call back into native code (throwFunc). + // 3. Re-throw the exception from throwFunc. + // This works because: + // 1. setImmediate queues the callback, and that queue is drained immediately following the invocation of the function passed to CallInvoker::invokeAsync. + // 2. The immediates queue is drained as part of the class bridge, which knows how to display the logbox for unhandled exceptions. + // In the future, CallInvoker::invokeAsync likely will properly integrate with logbox, at which point we can remove the try/catch and just call func directly. + dispatcher([env, &jsiRuntime, isRunning{ std::move(isRunning) }, func{ std::move(func) }] + { + try + { + // If JS engine shutdown is in progress, don't dispatch any new work. + if (*isRunning) + { + func(env); + } + } + catch (...) + { + auto ex{std::current_exception()}; + auto setImmediate{jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "setImmediate")}; + auto throwFunc{jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "throwFunc"), 0, + [ex](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value + { + std::rethrow_exception(ex); + })}; + setImmediate.call(jsiRuntime, {std::move(throwFunc)}); + } + }); + }; + } +} diff --git a/react-native/shared/XrAnchorHelper.h b/react-native/shared/XrAnchorHelper.h new file mode 100644 index 000000000..2e68748ea --- /dev/null +++ b/react-native/shared/XrAnchorHelper.h @@ -0,0 +1,273 @@ +#pragma once + +#if __has_include("jsi/jsi.h") +#include "jsi/jsi.h" +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(facebook::jsi::Runtime& jsiRuntime, facebook::jsi::Value& jsAnchor, uintptr_t& nativeAnchorPtr) + { + nativeAnchorPtr = reinterpret_cast(nullptr); + if (!jsAnchor.isObject()) + { + return false; + } + + if (!jsiRuntime.global().hasProperty(jsiRuntime, "navigator")) + { + return false; + } + + auto navigator{ jsiRuntime.global().getProperty(jsiRuntime, "navigator").asObject(jsiRuntime) }; + if (!navigator.hasProperty(jsiRuntime, "xr")) + { + return false; + } + + auto nativeXr{ navigator.getProperty(jsiRuntime, "xr").asObject(jsiRuntime) }; + if (!nativeXr.hasProperty(jsiRuntime, "getNativeAnchor")) + { + return false; + } + + auto getNativeAnchor{nativeXr.getPropertyAsFunction(jsiRuntime, "getNativeAnchor")}; + nativeAnchorPtr = static_cast(getNativeAnchor.call(jsiRuntime, { jsAnchor.asObject(jsiRuntime) }).asNumber()); + return true; + } +} +#endif + +#if __has_include("napi/env.h") +#include "napi/env.h" +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(Napi::Env env, Napi::Value anchor, uintptr_t& nativeAnchorPtr) + { + nativeAnchorPtr = reinterpret_cast(nullptr); + if (!anchor.IsObject()) + { + return false; + } + + if (!env.Global().Has("navigator")) + { + return false; + } + + auto navigator{ env.Global().Get("navigator").ToObject() }; + if (!navigator.Has("xr")) + { + return false; + } + + auto nativeXr{ navigator.Get("xr").ToObject() }; + if (!nativeXr.Has("getNativeAnchor")) + { + return false; + } + + auto getNativeAnchor{nativeXr.Get("getNativeAnchor").As()}; + nativeAnchorPtr = static_cast(getNativeAnchor.Call({ anchor }).As().DoubleValue()); + return true; + } + + bool TryDeclareNativeAnchor(Napi::Env env, const Napi::Value& session, uintptr_t nativeAnchorPtr, Napi::Value& xrAnchor) + { + xrAnchor = env.Undefined(); + if (!session.IsObject()) + { + return false; + } + + if (!env.Global().Has("navigator")) + { + return false; + } + + auto navigator{ env.Global().Get("navigator").ToObject() }; + if (!navigator.Has("xr")) + { + return false; + } + + auto nativeXr{ navigator.Get("xr").ToObject() }; + if (!nativeXr.Has("declareNativeAnchor")) + { + return false; + } + + auto declareNativeAnchor{nativeXr.Get("declareNativeAnchor").As()}; + xrAnchor = declareNativeAnchor.Call({ session, Napi::Number::From(env, nativeAnchorPtr) }); + return true; + } +} +#endif + +#if __has_include("IXrContextOpenXR.h") +#include "IXrContextOpenXR.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(facebook::jsi::Runtime& jsiRuntime, facebook::jsi::Value& jsAnchor, XrSpatialAnchorMSFT& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(jsiRuntime, jsAnchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(Napi::Env env, Napi::Value anchor, XrSpatialAnchorMSFT& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(env, anchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } +} +#endif +#endif + +#if __has_include("IXrContextARCore.h") +#include "IXrContextARCore.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(facebook::jsi::Runtime& jsiRuntime, facebook::jsi::Value& jsAnchor, ArAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(jsiRuntime, jsAnchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(Napi::Env env, Napi::Value anchor, ArAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(env, anchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } + + bool TryDeclareNativeAnchor(Napi::Env env, const Napi::Value& session, ArAnchor* nativeAnchor, Napi::Value& xrAnchor) + { + uintptr_t nativeAnchorPtr{reinterpret_cast(nativeAnchor)}; + return TryDeclareNativeAnchor(env, session, nativeAnchorPtr, xrAnchor); + } +} +#endif +#endif + +#if __has_include("IXrContextARKit.h") +#include "IXrContextARKit.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(facebook::jsi::Runtime& jsiRuntime, facebook::jsi::Value& jsAnchor, ARAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(jsiRuntime, jsAnchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(Napi::Env env, Napi::Value anchor, ARAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(env, anchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } + + bool TryDeclareNativeAnchor(Napi::Env env, const Napi::Value& session, ARAnchor* nativeAnchor, Napi::Value& xrAnchor) + { + uintptr_t nativeAnchorPtr{reinterpret_cast(nativeAnchor)}; + return TryDeclareNativeAnchor(env, session, nativeAnchorPtr, xrAnchor); + } +} +#endif +#endif + +#if __has_include("IXrContextAREngine.h") +#include "IXrContextAREngine.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(facebook::jsi::Runtime& jsiRuntime, facebook::jsi::Value& jsAnchor, ArAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(jsiRuntime, jsAnchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetNativeAnchor(Napi::Env env, Napi::Value anchor, ArAnchor*& nativeAnchor) + { + nativeAnchor = nullptr; + uintptr_t nativeAnchorPtr{reinterpret_cast(nullptr)}; + if (TryGetNativeAnchor(env, anchor, nativeAnchorPtr)) + { + nativeAnchor = reinterpret_cast(nativeAnchorPtr); + return true; + } + + return false; + } + + bool TryDeclareNativeAnchor(Napi::Env env, const Napi::Value& session, ArAnchor* nativeAnchor, Napi::Value& xrAnchor) + { + uintptr_t nativeAnchorPtr{reinterpret_cast(nativeAnchor)}; + return TryDeclareNativeAnchor(env, session, nativeAnchorPtr, xrAnchor); + } +} +#endif +#endif \ No newline at end of file diff --git a/react-native/shared/XrContextHelper.h b/react-native/shared/XrContextHelper.h new file mode 100644 index 000000000..b6e2a7eab --- /dev/null +++ b/react-native/shared/XrContextHelper.h @@ -0,0 +1,217 @@ +#pragma once + +#if __has_include("jsi/jsi.h") +#include "jsi/jsi.h" +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(facebook::jsi::Runtime& jsiRuntime, const std::string nativeXrContextType, uintptr_t& nativeXrContext) + { + nativeXrContext = reinterpret_cast(nullptr); + if (!jsiRuntime.global().hasProperty(jsiRuntime, "navigator")) + { + return false; + } + + auto navigator{ jsiRuntime.global().getProperty(jsiRuntime, "navigator").asObject(jsiRuntime) }; + if (!navigator.hasProperty(jsiRuntime, "xr")) + { + return false; + } + + auto nativeXr{ navigator.getProperty(jsiRuntime, "xr").asObject(jsiRuntime) }; + if (!nativeXr.hasProperty(jsiRuntime, "nativeXrContext") || + !nativeXr.hasProperty(jsiRuntime, "nativeXrContextType") || + nativeXr.getProperty(jsiRuntime, "nativeXrContextType").asString(jsiRuntime).utf8(jsiRuntime) != nativeXrContextType) + { + return false; + } + + nativeXrContext = static_cast(nativeXr.getProperty(jsiRuntime, "nativeXrContext").asNumber()); + return true; + } +} +#endif + +#if __has_include("napi/env.h") +#include "napi/env.h" +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(Napi::Env env, const std::string nativeXrContextType, uintptr_t& nativeXrContext) + { + nativeXrContext = reinterpret_cast(nullptr); + if (!env.Global().Has("navigator")) + { + return false; + } + + auto navigator{ env.Global().Get("navigator").As() }; + if (!navigator.Has("xr")) + { + return false; + } + + auto nativeXr{ navigator.Get("xr").As() }; + if (!nativeXr.Has("nativeXrContext") || + !nativeXr.Has("nativeXrContextType") || + nativeXr.Get("nativeXrContextType").As().Utf8Value() != nativeXrContextType) + { + return false; + } + + nativeXrContext = static_cast(nativeXr.Get("nativeXrContext").As().DoubleValue()); + return true; + } +} +#endif + +#if __has_include("IXrContextOpenXR.h") +#include "IXrContextOpenXR.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(facebook::jsi::Runtime& jsiRuntime, IXrContextOpenXR*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(jsiRuntime, "OpenXR", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(Napi::Env env, IXrContextOpenXR*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(env, "OpenXR", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#endif + +#if __has_include("IXrContextARCore.h") +#include "IXrContextARCore.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(facebook::jsi::Runtime& jsiRuntime, IXrContextARCore*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(jsiRuntime, "ARCore", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(Napi::Env env, IXrContextARCore*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(env, "ARCore", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#endif + +#if __has_include("IXrContextARKit.h") +#include "IXrContextARKit.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(facebook::jsi::Runtime& jsiRuntime, IXrContextARKit*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(jsiRuntime, "ARKit", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(Napi::Env env, IXrContextARKit*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(env, "ARKit", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#endif + +#if __has_include("IXrContextAREngine.h") +#include "IXrContextAREngine.h" +#if __has_include("jsi/jsi.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(facebook::jsi::Runtime& jsiRuntime, IXrContextAREngine*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(jsiRuntime, "AREngine", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#if __has_include("napi/env.h") +namespace Babylon::Plugins::NativeXr +{ + bool TryGetXrContext(Napi::Env env, IXrContextAREngine*& xrContext) + { + xrContext = nullptr; + uintptr_t nativePtr{reinterpret_cast(nullptr)}; + if (TryGetXrContext(env, "AREngine", nativePtr)) + { + xrContext = reinterpret_cast(nativePtr); + return true; + } + + return false; + } +} +#endif +#endif \ No newline at end of file diff --git a/react-native/tsconfig.json b/react-native/tsconfig.json new file mode 100644 index 000000000..328112e5c --- /dev/null +++ b/react-native/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["es6", "DOM"], + "moduleResolution": "node", + "noEmit": true, + "strict": true, + "resolveJsonModule": true, + "target": "esnext", + "skipLibCheck": true, + "sourceMap": true, + "declaration": true + }, + "exclude": [ + "node_modules", + "submodules", + "babel.config.js", + "metro.config.js", + "jest.config.js" + ] +} \ No newline at end of file