From fb453dc7c0511f49ebd845156a4caaa609d72185 Mon Sep 17 00:00:00 2001 From: kgv Date: Tue, 30 Mar 2021 14:33:36 +0300 Subject: [PATCH] 0032267: Samples - convert JNIViewer sample from Java to Kotlin --- samples/kotlin/jniviewer/.gitattributes | 1 + samples/kotlin/jniviewer/.gitignore | 10 + samples/kotlin/jniviewer/ReadMe.md | 38 + samples/kotlin/jniviewer/app/build.gradle | 53 ++ .../app/src/main/AndroidManifest.xml | 43 ++ .../opencascade/jnisample/OcctJniActivity.kt | 692 +++++++++++++++++ .../jnisample/OcctJniFileDialog.kt | 213 ++++++ .../opencascade/jnisample/OcctJniLogger.kt | 68 ++ .../opencascade/jnisample/OcctJniRenderer.kt | 186 +++++ .../com/opencascade/jnisample/OcctJniView.kt | 250 ++++++ .../jniviewer/app/src/main/jni/CMakeLists.txt | 44 ++ .../app/src/main/jni/OcctJni_MsgPrinter.cxx | 79 ++ .../app/src/main/jni/OcctJni_MsgPrinter.hxx | 56 ++ .../app/src/main/jni/OcctJni_Viewer.cxx | 718 ++++++++++++++++++ .../app/src/main/jni/OcctJni_Viewer.hxx | 99 +++ .../src/main/res/drawable-hdpi/close_l.png | Bin 0 -> 1410 bytes .../src/main/res/drawable-hdpi/close_p.png | Bin 0 -> 1325 bytes .../app/src/main/res/drawable-hdpi/fit.png | Bin 0 -> 1002 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 2591 bytes .../app/src/main/res/drawable-hdpi/info.png | Bin 0 -> 1660 bytes .../src/main/res/drawable-hdpi/info_image.png | Bin 0 -> 24204 bytes .../src/main/res/drawable-hdpi/message.png | Bin 0 -> 443 bytes .../app/src/main/res/drawable-hdpi/open.png | Bin 0 -> 917 bytes .../app/src/main/res/drawable-hdpi/open_l.png | Bin 0 -> 1415 bytes .../app/src/main/res/drawable-hdpi/open_p.png | Bin 0 -> 1322 bytes .../src/main/res/drawable-hdpi/proj_back.png | Bin 0 -> 1716 bytes .../main/res/drawable-hdpi/proj_bottom.png | Bin 0 -> 1867 bytes .../src/main/res/drawable-hdpi/proj_front.png | Bin 0 -> 1604 bytes .../src/main/res/drawable-hdpi/proj_left.png | Bin 0 -> 1771 bytes .../src/main/res/drawable-hdpi/proj_right.png | Bin 0 -> 1644 bytes .../src/main/res/drawable-hdpi/proj_top.png | Bin 0 -> 1862 bytes .../app/src/main/res/drawable-hdpi/view.png | Bin 0 -> 839 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1609 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 3669 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 5560 bytes .../app/src/main/res/layout/activity_main.xml | 166 ++++ .../jniviewer/app/src/main/res/values/id.xml | 17 + .../app/src/main/res/values/strings.xml | 34 + samples/kotlin/jniviewer/build.gradle | 19 + .../jniviewer/gradle.properties.template | 5 + samples/kotlin/jniviewer/settings.gradle | 1 + 41 files changed, 2792 insertions(+) create mode 100644 samples/kotlin/jniviewer/.gitattributes create mode 100644 samples/kotlin/jniviewer/.gitignore create mode 100644 samples/kotlin/jniviewer/ReadMe.md create mode 100644 samples/kotlin/jniviewer/app/build.gradle create mode 100644 samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml create mode 100644 samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt create mode 100644 samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt create mode 100644 samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt create mode 100644 samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt create mode 100644 samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt create mode 100644 samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt create mode 100644 samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx create mode 100644 samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx create mode 100644 samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx create mode 100644 samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_p.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/fit.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info_image.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_bottom.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_left.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_top.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/view.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 samples/kotlin/jniviewer/app/src/main/res/layout/activity_main.xml create mode 100644 samples/kotlin/jniviewer/app/src/main/res/values/id.xml create mode 100644 samples/kotlin/jniviewer/app/src/main/res/values/strings.xml create mode 100644 samples/kotlin/jniviewer/build.gradle create mode 100644 samples/kotlin/jniviewer/gradle.properties.template create mode 100644 samples/kotlin/jniviewer/settings.gradle diff --git a/samples/kotlin/jniviewer/.gitattributes b/samples/kotlin/jniviewer/.gitattributes new file mode 100644 index 0000000000..c275dc2521 --- /dev/null +++ b/samples/kotlin/jniviewer/.gitattributes @@ -0,0 +1 @@ +*.gradle eol=lf diff --git a/samples/kotlin/jniviewer/.gitignore b/samples/kotlin/jniviewer/.gitignore new file mode 100644 index 0000000000..3328b0f384 --- /dev/null +++ b/samples/kotlin/jniviewer/.gitignore @@ -0,0 +1,10 @@ +/.gradle +/.idea +/build +/gradle +gradlew +gradlew.bat +/app/.cxx +/app/build +gradle.properties +local.properties diff --git a/samples/kotlin/jniviewer/ReadMe.md b/samples/kotlin/jniviewer/ReadMe.md new file mode 100644 index 0000000000..ceeb123b62 --- /dev/null +++ b/samples/kotlin/jniviewer/ReadMe.md @@ -0,0 +1,38 @@ +OCCT JniViewer Kotlin sample for Android {#samples_kotlin_android_occt} +================== + +This sample demonstrates simple way of using OCCT libraries in Android application written using Kotlin. + +The connection between Kotlin and OCCT (C++) level is provided by proxy library, libTKJniSample.so, written in C++ with exported JNI methods of Kotlin class OcctJniRenderer. +The proxy library contains single C++ class OcctJni_Viewer encapsulating OCCT viewer and providing functionality to manipulate this viewer +and to import OCCT shapes from several supported formats of CAD files (IGES, STEP, BREP). + +This sample demonstrates indirect method of wrapping C++ to Kotlin using manually created proxy library. + +Install Android Studio 4.0+ and building tools (check Tools -> SDK Manager): +- Android SDK (API level 21 or higher). +- Android SDK build tools. +- Android NDK r16 or higher (coming with CMake toolchain). + Using NDK r18 or newer will require changing ANDROID_STL in project settings. +- CMake 3.10+. + +Specify this folder location in Android Studio for opening project. +You might need re-entering Android SDK explicitly in File -> Project Structure -> SDK Location settings (SDK, NDK, JDK locations). + +This sample expects OCCT to be already build - please refer to appropriate CMake building instructions in OCCT documentation. +The following variables should be added into file gradle.properties (see gradle.properties.template as template): +- `OCCT_ROOT` - path to OCCT installation folder. +- `FREETYPE_ROOT` - path to FreeType installation folder. + +FreeImage is optional and does not required for this sample, however you should include all extra libraries used for OCCT building +and load the explicitly from Java code within OcctJniActivity::loadNatives() method, including toolkits from OCCT itself in proper order: +~~~~ + if (!loadLibVerbose ("TKernel", aLoaded, aFailed) + || !loadLibVerbose ("TKMath", aLoaded, aFailed) + || !loadLibVerbose ("TKG2d", aLoaded, aFailed) +~~~~ +Note that C++ STL library is not part of Android system, and application must package this library as well as extra component ("gnustl_shared" by default - see also `ANDROID_STL`). + +After successful build via Build -> Rebuild Project, the application can be packaged to Android: +- Deploy and run application on connected device or emulator directly from Android Studio using adb interface by menu items "Run" and "Debug". This would sign package with debug certificate. +- Prepare signed end-user package using wizard Build -> Generate signed APK. diff --git a/samples/kotlin/jniviewer/app/build.gradle b/samples/kotlin/jniviewer/app/build.gradle new file mode 100644 index 0000000000..277b8b4671 --- /dev/null +++ b/samples/kotlin/jniviewer/app/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 21 + buildToolsVersion "30.0.0" + + defaultConfig { + applicationId "com.opencascade.jnisample" + minSdkVersion 21 + targetSdkVersion 26 + + ndk { + abiFilters "arm64-v8a" + } + + externalNativeBuild { + cmake { + arguments "-DOCCT_ROOT=" + OCCT_ROOT, + "-DFREETYPE_ROOT=" + FREETYPE_ROOT, + "-DANDROID_STL=gnustl_shared" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + sourceSets { + main { + manifest.srcFile 'src/main/AndroidManifest.xml' + assets.srcDirs = [OCCT_ROOT + "/src"] + } + } + + externalNativeBuild { + cmake { + path "src/main/jni/CMakeLists.txt" + } + } +} + +dependencies { + implementation fileTree(dir: 'java/com/opencascade/jnisample', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml b/samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c25a6d6389 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt new file mode 100644 index 0000000000..714c187d8c --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt @@ -0,0 +1,692 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +package com.opencascade.jnisample + +import android.Manifest +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.content.res.Configuration +import android.graphics.Point +import android.os.Bundle +import android.os.Environment +import android.text.Html +import android.text.Html.ImageGetter +import android.util.TypedValue +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import java.io.* +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.util.* +import kotlin.jvm.Throws + +//! Main activity +class OcctJniActivity : Activity(), View.OnClickListener { + //! Auxiliary method to load native libraries + fun loadNatives(): Boolean { + if (wasNativesLoadCalled) { + return areNativeLoaded + } + wasNativesLoadCalled = true + val aLoaded = StringBuilder() + val aFailed = StringBuilder() + + // copy OCCT resources + val aResFolder = filesDir.absolutePath + copyAssetFolder(assets, "src/SHMessage", "$aResFolder/SHMessage") + copyAssetFolder(assets, "src/XSMessage", "$aResFolder/XSMessage") + + // C++ runtime + loadLibVerbose("gnustl_shared", aLoaded, aFailed) + + // 3rd-parties + loadLibVerbose("freetype", aLoaded, aFailed) + loadLibVerbose("freeimage", aLoaded, aFailed) + if ( // OCCT modeling + !loadLibVerbose("TKernel", aLoaded, aFailed) + || !loadLibVerbose("TKMath", aLoaded, aFailed) + || !loadLibVerbose("TKG2d", aLoaded, aFailed) + || !loadLibVerbose("TKG3d", aLoaded, aFailed) + || !loadLibVerbose("TKGeomBase", aLoaded, aFailed) + || !loadLibVerbose("TKBRep", aLoaded, aFailed) + || !loadLibVerbose("TKGeomAlgo", aLoaded, aFailed) + || !loadLibVerbose("TKTopAlgo", aLoaded, aFailed) + || !loadLibVerbose("TKShHealing", aLoaded, aFailed) + || !loadLibVerbose("TKMesh", aLoaded, aFailed) // exchange + || !loadLibVerbose("TKPrim", aLoaded, aFailed) + || !loadLibVerbose("TKBO", aLoaded, aFailed) + || !loadLibVerbose("TKBool", aLoaded, aFailed) + || !loadLibVerbose("TKFillet", aLoaded, aFailed) + || !loadLibVerbose("TKOffset", aLoaded, aFailed) + || !loadLibVerbose("TKXSBase", aLoaded, aFailed) + || !loadLibVerbose("TKSTL", aLoaded, aFailed) + || !loadLibVerbose("TKIGES", aLoaded, aFailed) + || !loadLibVerbose("TKSTEPBase", aLoaded, aFailed) + || !loadLibVerbose("TKSTEPAttr", aLoaded, aFailed) + || !loadLibVerbose("TKSTEP209", aLoaded, aFailed) + || !loadLibVerbose("TKSTEP", aLoaded, aFailed) // OCCT Visualization + || !loadLibVerbose("TKService", aLoaded, aFailed) + || !loadLibVerbose("TKHLR", aLoaded, aFailed) + || !loadLibVerbose("TKV3d", aLoaded, aFailed) + || !loadLibVerbose("TKOpenGles", aLoaded, aFailed) // application code + || !loadLibVerbose("TKJniSample", aLoaded, aFailed)) { + nativeLoaded = aLoaded.toString() + nativeFailed = aFailed.toString() + areNativeLoaded = false + //exitWithError (theActivity, "Broken apk?\n" + theFailedInfo); + return false + } + nativeLoaded = aLoaded.toString() + areNativeLoaded = true + return true + } + + //! Create activity + override fun onCreate(theBundle: Bundle?) { + super.onCreate(theBundle) + val isLoaded = loadNatives() + if (!isLoaded) { + printShortInfo(this, nativeFailed) + OcctJniLogger.postMessage(""" + $nativeLoaded + $nativeFailed + """.trimIndent()) + } + setContentView(R.layout.activity_main) + myOcctView = findViewById(R.id.custom_view) as OcctJniView + myMessageTextView = findViewById(R.id.message_view) as TextView + OcctJniLogger.setTextView(myMessageTextView) + createViewAndButtons(Configuration.ORIENTATION_LANDSCAPE) + myButtonPreferSize = defineButtonSize(findViewById(R.id.panel_menu) as LinearLayout) + val aScrollBtn = findViewById(R.id.scroll_btn) as ImageButton + aScrollBtn.y = myButtonPreferSize.toFloat() + aScrollBtn.setOnTouchListener { theView, theEvent -> onScrollBtnTouch(theView, theEvent) } + onConfigurationChanged(resources.configuration) + val anIntent = intent + val aDataUrl = anIntent?.data + val aDataPath = if (aDataUrl != null) aDataUrl.path else "" + myOcctView!!.open(aDataPath) + myLastPath = aDataPath + myContext = ContextWrapper(this) + myContext!!.getExternalFilesDir(null) + } + + //! Handle scroll events + private fun onScrollBtnTouch(theView: View, + theEvent: MotionEvent): Boolean { + when (theEvent.action) { + MotionEvent.ACTION_DOWN -> { + val aPanelMenu = findViewById(R.id.panel_menu) as LinearLayout + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + if (aPanelMenu.visibility == View.VISIBLE) { + aPanelMenu.visibility = View.GONE + if (!isLandscape) { + (theView as ImageButton).setImageResource(R.drawable.open_p) + theView.setY(0f) + } else { + (theView as ImageButton).setImageResource(R.drawable.open_l) + theView.setX(0f) + } + } else { + aPanelMenu.visibility = View.VISIBLE + if (!isLandscape) { + (theView as ImageButton).setImageResource(R.drawable.close_p) + theView.setY(myButtonPreferSize.toFloat()) + } else { + (theView as ImageButton).setImageResource(R.drawable.close_l) + theView.setX(myButtonPreferSize.toFloat()) + } + } + } + } + return false + } + + //! Initialize views and buttons + @Suppress("UNUSED_PARAMETER") + private fun createViewAndButtons(theOrientation: Int) { + // open button + val anOpenButton = findViewById(R.id.open) as ImageButton + anOpenButton.setOnClickListener(this) + + // fit all + val aFitAllButton = findViewById(R.id.fit) as ImageButton + aFitAllButton.setOnClickListener(this) + aFitAllButton.setOnTouchListener { theView, theEvent -> onTouchButton(theView, theEvent) } + + // message + val aMessageButton = findViewById(R.id.message) as ImageButton + aMessageButton.setOnClickListener(this) + + // info + val anInfoButton = findViewById(R.id.info) as ImageButton + anInfoButton.setOnClickListener(this) + + // font for text view + val anInfoView = findViewById(R.id.info_view) as TextView + anInfoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) + + // add submenu buttons + createSubmenuBtn(R.id.view, R.id.view_group, + Arrays.asList(R.id.proj_front, R.id.proj_top, R.id.proj_left, + R.id.proj_back, R.id.proj_bottom, R.id.proj_right), + Arrays.asList(R.drawable.proj_front, R.drawable.proj_top, R.drawable.proj_left, + R.drawable.proj_back, R.drawable.proj_bottom, R.drawable.proj_right), + 4) + } + + override fun onNewIntent(theIntent: Intent) { + super.onNewIntent(theIntent) + intent = theIntent + } + + override fun onDestroy() { + super.onDestroy() + OcctJniLogger.setTextView(null) + } + + override fun onPause() { + super.onPause() + myOcctView!!.onPause() + } + + override fun onResume() { + super.onResume() + myOcctView!!.onResume() + val anIntent = intent + val aDataUrl = anIntent?.data + val aDataPath = if (aDataUrl != null) aDataUrl.path else "" + if (aDataPath != myLastPath) { + myOcctView!!.open(aDataPath) + myLastPath = aDataPath + } + } + + //! Copy folder from assets + private fun copyAssetFolder(theAssetMgr: AssetManager, + theAssetFolder: String, + theFolderPathTo: String): Boolean { + return try { + val aFiles = theAssetMgr.list(theAssetFolder) + val aFolder = File(theFolderPathTo) + aFolder.mkdirs() + var isOk = true + for (aFileIter in aFiles) { + isOk = if (aFileIter.contains(".")) { + isOk and copyAsset(theAssetMgr, + "$theAssetFolder/$aFileIter", + "$theFolderPathTo/$aFileIter") + } else { + isOk and copyAssetFolder(theAssetMgr, + "$theAssetFolder/$aFileIter", + "$theFolderPathTo/$aFileIter") + } + } + isOk + } catch (theError: Exception) { + theError.printStackTrace() + false + } + } + + //! Copy single file from assets + private fun copyAsset(theAssetMgr: AssetManager, + thePathFrom: String, + thePathTo: String): Boolean { + return try { + val aStreamIn = theAssetMgr.open(thePathFrom) + val aFileTo = File(thePathTo) + aFileTo.createNewFile() + val aStreamOut: OutputStream? = FileOutputStream(thePathTo) + copyStreamContent(aStreamIn, aStreamOut) + aStreamIn.close() + aStreamOut!!.flush() + aStreamOut.close() + true + } catch (theError: Exception) { + theError.printStackTrace() + false + } + } + + //! Show/hide text view + private fun switchTextView(theTextView: TextView?, + theClickedBtn: ImageButton, + theToSwitchOn: Boolean) { + if (theTextView != null && theTextView.visibility == View.GONE && theToSwitchOn) { + theTextView.visibility = View.VISIBLE + theClickedBtn.setBackgroundColor(resources.getColor(R.color.pressedBtnColor)) + setTextViewPosition(theTextView) + } else { + theTextView!!.visibility = View.GONE + theClickedBtn.setBackgroundColor(resources.getColor(R.color.btnColor)) + } + } + + //! Setup text view position + private fun setTextViewPosition(theTextView: TextView?) { + if (theTextView!!.visibility != View.VISIBLE) { + return + } + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + theTextView.x = myButtonPreferSize.toFloat() + theTextView.y = 0f + } else { + theTextView.x = 0f + theTextView.y = myButtonPreferSize.toFloat() + } + } + + override fun onClick(theButton: View) { + val aClickedBtn = theButton as ImageButton + when (aClickedBtn.id) { + R.id.message -> { + switchTextView(findViewById(R.id.info_view) as TextView, + findViewById(R.id.info) as ImageButton, false) + switchTextView(myMessageTextView, aClickedBtn, true) + return + } + R.id.info -> { + var aText = getString(R.string.info_html) + aText = String.format(aText, cppOcctMajorVersion(), cppOcctMinorVersion(), cppOcctMicroVersion()) + val aSpanned = Html.fromHtml(aText, ImageGetter { theSource -> + val aResources = resources + val anId = aResources.getIdentifier(theSource, "drawable", packageName) + val aRes = aResources.getDrawable(anId) + aRes.setBounds(0, 0, aRes.intrinsicWidth, aRes.intrinsicHeight) + aRes + }, null) + val anInfoView = findViewById(R.id.info_view) as TextView + anInfoView.text = aSpanned + switchTextView(myMessageTextView, findViewById(R.id.message) as ImageButton, false) + switchTextView(anInfoView, aClickedBtn, true) + return + } + R.id.fit -> { + myOcctView!!.fitAll() + return + } + R.id.proj_front -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Xpos) + return + } + R.id.proj_left -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Yneg) + return + } + R.id.proj_top -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Zpos) + return + } + R.id.proj_back -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Xneg) + return + } + R.id.proj_right -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Ypos) + return + } + R.id.proj_bottom -> { + myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Zneg) + return + } + R.id.open -> { + val aPath = Environment.getExternalStorageDirectory() + aClickedBtn.setBackgroundColor(resources.getColor(R.color.pressedBtnColor)) + if (myFileOpenDialog == null) { + // should be requested on runtime since API level 26 (Android 8) + askUserPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, null) // for accessing SD card + myFileOpenDialog = OcctJniFileDialog(this, aPath) + myFileOpenDialog!!.setFileEndsWith(".brep") + myFileOpenDialog!!.setFileEndsWith(".rle") + myFileOpenDialog!!.setFileEndsWith(".iges") + myFileOpenDialog!!.setFileEndsWith(".igs") + myFileOpenDialog!!.setFileEndsWith(".step") + myFileOpenDialog!!.setFileEndsWith(".stp") + myFileOpenDialog!!.setFileEndsWith(".stl") + + myFileOpenDialog!!.addFileListener (object : OcctJniFileDialog.FileSelectedListener { + override fun fileSelected(theFile: File?) { + if (theFile != null && myOcctView != null) { + myOcctView!!.open(theFile.getPath()) + } + } + }) + + myFileOpenDialog!!.addDialogDismissedListener (object : OcctJniFileDialog.DialogDismissedListener { + override fun dialogDismissed() { + val openButton = findViewById(R.id.open) as ImageButton + openButton.setBackgroundColor(resources.getColor(R.color.btnColor)) + } + }) + } + myFileOpenDialog!!.showDialog() + return + } + } + } + + @Suppress("UNUSED_PARAMETER") + private fun createSubmenuBtn(theParentBtnId: Int, + theParentLayoutId: Int, + theNewButtonIds: List, + theNewButtonImageIds: List, + thePosition: Int) { + var aPosInList = 0 + val aParentBtn = findViewById(theParentBtnId) as? ImageButton + val aParams: ViewGroup.LayoutParams? = null + val parentLayout = findViewById(theParentLayoutId) as LinearLayout + for (newButtonId in theNewButtonIds) { + var aNewButton = findViewById(newButtonId) as? ImageButton + if (aNewButton == null) { + aNewButton = ImageButton(this) + aNewButton.id = newButtonId + aNewButton.setImageResource(theNewButtonImageIds[aPosInList]) + aNewButton.layoutParams = aParams + parentLayout.addView(aNewButton) + } + aNewButton.setOnClickListener(this) + aNewButton.visibility = View.GONE + aNewButton.setOnTouchListener { theView, theEvent -> onTouchButton(theView, theEvent) } + ++aPosInList + } + if (aParentBtn != null) { + aParentBtn.setOnTouchListener(null) + aParentBtn.setOnTouchListener { theView, theEvent -> + if (theView == null) {} // dummy + if (theEvent.action == MotionEvent.ACTION_DOWN) { + var isVisible = false + for (aNewButtonId in theNewButtonIds) { + val anBtn = findViewById(aNewButtonId) as? ImageButton + if (anBtn != null) { + if (anBtn.visibility == View.GONE) { + anBtn.visibility = View.VISIBLE + isVisible = true + } else { + anBtn.visibility = View.GONE + } + } + } + aParentBtn.setBackgroundColor(if (!isVisible) resources.getColor(R.color.btnColor) else resources.getColor(R.color.pressedBtnColor)) + } + false + } + } + } + + //! Implements onTouch functionality + private fun onTouchButton(theView: View, + theEvent: MotionEvent): Boolean { + when (theEvent.action) { + MotionEvent.ACTION_DOWN -> (theView as ImageButton).setBackgroundColor(resources.getColor(R.color.pressedBtnColor)) + MotionEvent.ACTION_UP -> (theView as ImageButton).setBackgroundColor(resources.getColor(R.color.btnColor)) + } + return false + } + + //! Handle configuration change event + override fun onConfigurationChanged(theNewConfig: Configuration) { + super.onConfigurationChanged(theNewConfig) + val aLayoutPanelMenu = findViewById(R.id.panel_menu) as LinearLayout + val aPanelMenuLayoutParams = aLayoutPanelMenu.layoutParams + val aLayoutViewGroup = findViewById(R.id.view_group) as LinearLayout + val aViewGroupLayoutParams = aLayoutViewGroup.layoutParams + val aScrollBtn = findViewById(R.id.scroll_btn) as ImageButton + val aScrollBtnLayoutParams = aScrollBtn.layoutParams + myButtonPreferSize = defineButtonSize(findViewById(R.id.panel_menu) as LinearLayout) + defineButtonSize(findViewById(R.id.view_group) as LinearLayout) + when (theNewConfig.orientation) { + Configuration.ORIENTATION_PORTRAIT -> { + setHorizontal(aLayoutPanelMenu, aPanelMenuLayoutParams) + setHorizontal(aLayoutViewGroup, aViewGroupLayoutParams) + aLayoutViewGroup.setGravity(Gravity.BOTTOM) + aScrollBtnLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + aScrollBtnLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + aScrollBtn.layoutParams = aScrollBtnLayoutParams + if (aLayoutPanelMenu.visibility == View.VISIBLE) { + aScrollBtn.setImageResource(R.drawable.close_p) + aScrollBtn.y = myButtonPreferSize.toFloat() + aScrollBtn.x = 0f + } else { + aScrollBtn.setImageResource(R.drawable.open_p) + aScrollBtn.y = 0f + aScrollBtn.x = 0f + } + } + Configuration.ORIENTATION_LANDSCAPE -> { + setVertical(aLayoutPanelMenu, aPanelMenuLayoutParams) + setVertical(aLayoutViewGroup, aViewGroupLayoutParams) + aLayoutViewGroup.setGravity(Gravity.RIGHT) + aScrollBtnLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + aScrollBtnLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT + aScrollBtn.layoutParams = aScrollBtnLayoutParams + if (aLayoutPanelMenu.visibility == View.VISIBLE) { + aScrollBtn.setImageResource(R.drawable.close_l) + aScrollBtn.x = myButtonPreferSize.toFloat() + aScrollBtn.y = 0f + } else { + aScrollBtn.setImageResource(R.drawable.open_l) + aScrollBtn.y = 0f + aScrollBtn.x = 0f + } + } + } + setTextViewPosition(myMessageTextView) + setTextViewPosition(findViewById(R.id.info_view) as TextView) + } + + private fun setHorizontal(theLayout: LinearLayout, + theLayoutParams: ViewGroup.LayoutParams) { + theLayout.orientation = LinearLayout.HORIZONTAL + theLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + theLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + theLayout.layoutParams = theLayoutParams + } + + private fun setVertical(theLayout: LinearLayout, + theLayoutParams: ViewGroup.LayoutParams) { + theLayout.orientation = LinearLayout.VERTICAL + theLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + theLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT + theLayout.layoutParams = theLayoutParams + } + + //! Define button size + private fun defineButtonSize(theLayout: LinearLayout): Int { + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val aDisplay = windowManager.defaultDisplay + val aDispPnt = Point() + aDisplay.getSize(aDispPnt) + val aNbChildren = theLayout.childCount + val aHeight = aDispPnt.y / aNbChildren + val aWidth = aDispPnt.x / aNbChildren + + for (aChildIter in 0 until aNbChildren) { + val aView = theLayout.getChildAt(aChildIter) + if (aView is ImageButton) { + val aButton = aView + if (isLandscape) { + aButton.minimumWidth = aHeight + } else { + aButton.minimumHeight = aWidth + } + } + } + return if (isLandscape) { aHeight } else { aWidth } + } + + //! Request user permission. + private fun askUserPermission(thePermission: String, theRationale: String?) { + // Dynamically load methods introduced by API level 23. + // On older system this permission is granted by user during application installation. + val aMetPtrCheckSelfPermission: Method + val aMetPtrRequestPermissions: Method + val aMetPtrShouldShowRequestPermissionRationale: Method + try { + aMetPtrCheckSelfPermission = myContext!!.javaClass.getMethod("checkSelfPermission", String::class.java) + aMetPtrRequestPermissions = javaClass.getMethod("requestPermissions", Array::class.java, Int::class.javaPrimitiveType) + aMetPtrShouldShowRequestPermissionRationale = javaClass.getMethod("shouldShowRequestPermissionRationale", String::class.java) + } catch (theError: SecurityException) { + postMessage(""" + Unable to find permission methods: + ${theError.message} + """.trimIndent(), Message_Trace) + return + } catch (theError: NoSuchMethodException) { + postMessage(""" + Unable to find permission methods: + ${theError.message} + """.trimIndent(), Message_Trace) + return + } + try { + val isAlreadyGranted = aMetPtrCheckSelfPermission.invoke(myContext, thePermission) as Int + if (isAlreadyGranted == PackageManager.PERMISSION_GRANTED) { + return + } + val toShowInfo = theRationale != null && aMetPtrShouldShowRequestPermissionRationale.invoke(this, thePermission) as Boolean + if (toShowInfo) { + postMessage(theRationale, Message_Info) + } + + // show dialog to user + aMetPtrRequestPermissions.invoke(this, arrayOf(thePermission), 0) + } catch (theError: IllegalArgumentException) { + postMessage(""" + Internal error: Unable to call permission method: + ${theError.message} + """.trimIndent(), Message_Fail) + return + } catch (theError: IllegalAccessException) { + postMessage(""" + Internal error: Unable to call permission method: + ${theError.message} + """.trimIndent(), Message_Fail) + return + } catch (theError: InvocationTargetException) { + postMessage(""" + Internal error: Unable to call permission method: + ${theError.message} + """.trimIndent(), Message_Fail) + return + } + } + + //! Auxiliary method to show info message. + fun postMessage(theMessage: String?, theGravity: Int) { + if (theGravity == Message_Trace) { + return + } + val aCtx: Context = this + runOnUiThread { + val aBuilder = AlertDialog.Builder(aCtx) + aBuilder.setMessage(theMessage).setNegativeButton("OK", null) + val aDialog = aBuilder.create() + aDialog.show() + } + } + + //! OCCT major version + private external fun cppOcctMajorVersion(): Long + + //! OCCT minor version + private external fun cppOcctMinorVersion(): Long + + //! OCCT micro version + private external fun cppOcctMicroVersion(): Long + private var myOcctView: OcctJniView? = null + private var myMessageTextView: TextView? = null + private var myLastPath: String? = null + private var myContext: ContextWrapper? = null + private var myFileOpenDialog: OcctJniFileDialog? = null + private var myButtonPreferSize = 65 + + companion object { + //! Auxiliary method to print temporary info messages + fun printShortInfo(theActivity: Activity, + theInfo: CharSequence?) { + val aCtx = theActivity.applicationContext + val aToast = Toast.makeText(aCtx, theInfo, Toast.LENGTH_LONG) + aToast.show() + } + + //! Load single native library + private fun loadLibVerbose(theLibName: String, + theLoadedInfo: StringBuilder, + theFailedInfo: StringBuilder): Boolean { + return try { + System.loadLibrary(theLibName) + theLoadedInfo.append("Info: native library \"") + theLoadedInfo.append(theLibName) + theLoadedInfo.append("\" has been loaded\n") + true + } catch (theError: UnsatisfiedLinkError) { + theFailedInfo.append("Error: native library \"") + theFailedInfo.append(theLibName) + theFailedInfo.append("""" is unavailable: + ${theError.message}""") + false + } catch (theError: SecurityException) { + theFailedInfo.append("Error: native library \"") + theFailedInfo.append(theLibName) + theFailedInfo.append("""" can not be loaded for security reasons: + ${theError.message}""") + false + } + } + + var wasNativesLoadCalled = false + @JvmField + var areNativeLoaded = false + var nativeLoaded = "" + var nativeFailed = "" + + //! Copy single file + @Throws(IOException::class) + private fun copyStreamContent(theIn: InputStream?, + theOut: OutputStream?) { + val aBuffer = ByteArray(1024) + var aNbReadBytes: Int + while (theIn!!.read(aBuffer).also { aNbReadBytes = it } != -1) { + theOut!!.write(aBuffer, 0, aNbReadBytes) + } + } + + //! Message gravity. + private const val Message_Trace = 0 + private const val Message_Info = 1 + private const val Message_Warning = 2 + private const val Message_Alarm = 3 + private const val Message_Fail = 4 + } +} diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt new file mode 100644 index 0000000000..bbbc11958e --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt @@ -0,0 +1,213 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +package com.opencascade.jnisample + +import android.app.Activity +import android.app.AlertDialog +import android.app.Dialog +import android.content.DialogInterface +import android.graphics.Color +import android.os.Environment +import android.view.ViewGroup +import android.widget.* +import android.widget.AdapterView.OnItemClickListener +import com.opencascade.jnisample.ListenerList.FireHandler +import java.io.File +import java.io.FilenameFilter +import java.util.* + +//! Simple open file dialog +class OcctJniFileDialog(theActivity: Activity, + thePath: File) { + + private var myFileList: Array? = emptyArray() + private var myCurrentPath: File? = null + private val myFileListenerList = ListenerList() + private val myDialogDismissedList = ListenerList() + private val myActivity: Activity + private var myFileEndsWith: MutableList? = null + + //! Main constructor. + init { + var aPath = thePath + myActivity = theActivity + if (!aPath.exists()) { + aPath = Environment.getExternalStorageDirectory() + } + loadFileList(aPath) + } + + interface FileSelectedListener { + fun fileSelected(theFile: File?) + } + + interface DialogDismissedListener { + fun dialogDismissed() + } + + //! Create new dialog + fun createFileDialog(): Dialog? { + val anObjWrapper = arrayOfNulls(1) + val aBuilder = AlertDialog.Builder(myActivity) + aBuilder.setTitle(myCurrentPath!!.path) + val aTitleLayout = LinearLayout(myActivity) + aTitleLayout.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + aTitleLayout.orientation = LinearLayout.VERTICAL + val list = ListView(myActivity) + list.isScrollingCacheEnabled = false + list.setBackgroundColor(Color.parseColor("#33B5E5")) + list.adapter = ArrayAdapter(myActivity, android.R.layout.select_dialog_item, myFileList) + list.onItemClickListener = OnItemClickListener { arg0, view, pos, id -> + if (arg0 == null || view == null || id == 0L) {} // dummy + val fileChosen = myFileList!![pos] + val aChosenFile = getChosenFile(fileChosen) + if (aChosenFile.isDirectory) { + loadFileList(aChosenFile) + (anObjWrapper[0] as Dialog?)!!.cancel() + (anObjWrapper[0] as Dialog?)!!.dismiss() + showDialog() + } else { + (anObjWrapper[0] as Dialog?)!!.cancel() + (anObjWrapper[0] as Dialog?)!!.dismiss() + fireFileSelectedEvent(aChosenFile) + } + } + list.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0.6f) + aTitleLayout.addView(list) + aBuilder.setNegativeButton("Cancel", null) + aBuilder.setView(aTitleLayout) + + val aDialog = aBuilder.show() + aDialog.setOnDismissListener(DialogInterface.OnDismissListener { fireDialogDismissedEvent() }) + anObjWrapper[0] = aDialog + return aDialog + } + + fun addFileListener(theListener: FileSelectedListener) { + myFileListenerList.add(theListener) + } + + fun addDialogDismissedListener(theListener: DialogDismissedListener) { + myDialogDismissedList.add(theListener) + } + + //! Show file dialog + fun showDialog() { + createFileDialog()!!.show() + } + + private fun fireFileSelectedEvent(theFile: File) { + myFileListenerList.fireEvent(object : FireHandler { + override fun fireEvent(theListener: FileSelectedListener) { + theListener.fileSelected(theFile) + } + }) + } + + private fun fireDialogDismissedEvent() { + myDialogDismissedList.fireEvent(object : FireHandler { + override fun fireEvent(theListener: DialogDismissedListener) { + theListener.dialogDismissed() + } + }) + } + + private fun loadFileList(thePath: File?) { + myCurrentPath = thePath + val aList: MutableList = ArrayList() + if (thePath!!.exists()) { + if (thePath.parentFile != null) { + aList.add(PARENT_DIR) + } + val aFilter = FilenameFilter { theDir, theFilename -> + val aSel = File(theDir, theFilename) + if (!aSel.canRead()) { + return@FilenameFilter false + } + var isEndWith = false + if (myFileEndsWith != null) { + for (aFileExtIter in myFileEndsWith!!) { + if (theFilename.toLowerCase().endsWith(aFileExtIter)) { + isEndWith = true + break + } + } + } + isEndWith || aSel.isDirectory + } + val aFileList1 = thePath.list(aFilter) + if (aFileList1 != null) { + for (aFileIter in aFileList1) { + aList.add(aFileIter) + } + } + } + myFileList = aList.toTypedArray() + } + + private fun getChosenFile(theFileChosen: String): File { + return if (theFileChosen == PARENT_DIR) myCurrentPath!!.parentFile else File(myCurrentPath, theFileChosen) + } + + fun setFileEndsWith(fileEndsWith: String) { + if (myFileEndsWith == null) { + myFileEndsWith = ArrayList() + } + if (myFileEndsWith!!.indexOf(fileEndsWith) == -1) { + myFileEndsWith!!.add(fileEndsWith) + } + } + + fun setFileEndsWith(theFileEndsWith: MutableList?) { + myFileEndsWith = theFileEndsWith + } + + companion object { + private const val PARENT_DIR = ".." + } +} + +internal class ListenerList { + private val myListenerList: MutableList = ArrayList() + + interface FireHandler { + fun fireEvent(theListener: L) + } + + fun add(theListener: L) { + myListenerList.add(theListener) + } + + fun fireEvent(theFireHandler: FireHandler) { + val aCopy: List = ArrayList(myListenerList) + for (anIter in aCopy) { + theFireHandler.fireEvent(anIter) + } + } + + fun remove(theListener: L) { + myListenerList.remove(theListener) + } + + val listenerList: List + get() = myListenerList +} diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt new file mode 100644 index 0000000000..f1bd6859d5 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt @@ -0,0 +1,68 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +package com.opencascade.jnisample + +import android.util.Log +import android.widget.TextView +import java.util.concurrent.locks.ReentrantLock + +//! Auxiliary class for logging messages +object OcctJniLogger { + //! Setup text view + fun setTextView(theTextView: TextView?) { + if (myTextView != null) { + myLog = myTextView!!.text.toString() + } + myTextView = theTextView + if (myTextView != null) { + myTextView!!.text = myLog + myLog = "" + } + } + + //! Interface implementation + @JvmStatic + fun postMessage(theText: String?) { + var aCopy = String() + aCopy += theText + Log.e(myTag, theText) + myMutex.lock() + val aView = myTextView + if (aView == null) { + myLog += aCopy + myMutex.unlock() + return + } + aView.post(Runnable { + aView.text = """ + ${aView.text}$aCopy + + """.trimIndent() + }) + myMutex.unlock() + } + + private const val myTag = "occtJniViewer" + private val myMutex = ReentrantLock(true) + private var myTextView: TextView? = null + private var myLog = "" +} diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt new file mode 100644 index 0000000000..3eeb7fa927 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt @@ -0,0 +1,186 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +package com.opencascade.jnisample + +import android.opengl.GLSurfaceView +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.opengles.GL10 + +//! Wrapper for C++ OCCT viewer. +class OcctJniRenderer internal constructor(theView: GLSurfaceView?, + theScreenDensity: Float) : GLSurfaceView.Renderer { + //! Wrapper for V3d_TypeOfOrientation + enum class TypeOfOrientation { + Xpos, // front + Ypos, // left + Zpos, // top + Xneg, // back + Yneg, // right + Zneg // bottom + } + + //! Open file. + fun open(thePath: String) { + if (myCppViewer != 0L) { + cppOpen(myCppViewer, thePath) + } + } + + //! Update viewer. + override fun onDrawFrame(theGl: GL10) { + if (myCppViewer != 0L) { + if (cppRedraw(myCppViewer)) { + myView!!.requestRender() // this method is allowed from any thread + } + } + } + + //! (re)initialize viewer. + override fun onSurfaceChanged(theGl: GL10, theWidth: Int, theHeight: Int) { + if (myCppViewer != 0L) { + cppResize(myCppViewer, theWidth, theHeight) + } + } + + override fun onSurfaceCreated(theGl: GL10, theEglConfig: EGLConfig) { + if (myCppViewer != 0L) { + cppInit(myCppViewer) + } + } + + //! Add touch point. + fun onAddTouchPoint(theId: Int, theX: Float, theY: Float) { + if (myCppViewer != 0L) { + cppAddTouchPoint(myCppViewer, theId, theX, theY) + } + } + + //! Update touch point. + fun onUpdateTouchPoint(theId: Int, theX: Float, theY: Float) { + if (myCppViewer != 0L) { + cppUpdateTouchPoint(myCppViewer, theId, theX, theY) + } + } + + //! Remove touch point. + fun onRemoveTouchPoint(theId: Int) { + if (myCppViewer != 0L) { + cppRemoveTouchPoint(myCppViewer, theId) + } + } + + //! Select in 3D Viewer. + fun onSelectInViewer(theX: Float, theY: Float) { + if (myCppViewer != 0L) { + cppSelectInViewer(myCppViewer, theX, theY) + } + } + + //! Fit All + fun fitAll() { + if (myCppViewer != 0L) { + cppFitAll(myCppViewer) + } + } + + //! Move camera + fun setProj(theProj: TypeOfOrientation?) { + if (myCppViewer == 0L) { + return + } + when (theProj) { + TypeOfOrientation.Xpos -> cppSetXposProj(myCppViewer) + TypeOfOrientation.Ypos -> cppSetYposProj(myCppViewer) + TypeOfOrientation.Zpos -> cppSetZposProj(myCppViewer) + TypeOfOrientation.Xneg -> cppSetXnegProj(myCppViewer) + TypeOfOrientation.Yneg -> cppSetYnegProj(myCppViewer) + TypeOfOrientation.Zneg -> cppSetZnegProj(myCppViewer) + } + } + + //! Post message to the text view. + fun postMessage(theText: String?) { + OcctJniLogger.postMessage(theText) + } + + //! Create instance of C++ class + private external fun cppCreate(theDispDensity: Float): Long + + //! Destroy instance of C++ class + private external fun cppDestroy(theCppPtr: Long) + + //! Initialize OCCT viewer (steal OpenGL ES context bound to this thread) + private external fun cppInit(theCppPtr: Long) + + //! Resize OCCT viewer + private external fun cppResize(theCppPtr: Long, theWidth: Int, theHeight: Int) + + //! Open CAD file + private external fun cppOpen(theCppPtr: Long, thePath: String) + + //! Add touch point + private external fun cppAddTouchPoint(theCppPtr: Long, theId: Int, theX: Float, theY: Float) + + //! Update touch point + private external fun cppUpdateTouchPoint(theCppPtr: Long, theId: Int, theX: Float, theY: Float) + + //! Remove touch point + private external fun cppRemoveTouchPoint(theCppPtr: Long, theId: Int) + + //! Select in 3D Viewer. + private external fun cppSelectInViewer(theCppPtr: Long, theX: Float, theY: Float) + + //! Redraw OCCT viewer + //! Returns TRUE if more frames are requested. + private external fun cppRedraw(theCppPtr: Long): Boolean + + //! Fit All + private external fun cppFitAll(theCppPtr: Long) + + //! Move camera + private external fun cppSetXposProj(theCppPtr: Long) + + //! Move camera + private external fun cppSetYposProj(theCppPtr: Long) + + //! Move camera + private external fun cppSetZposProj(theCppPtr: Long) + + //! Move camera + private external fun cppSetXnegProj(theCppPtr: Long) + + //! Move camera + private external fun cppSetYnegProj(theCppPtr: Long) + + //! Move camera + private external fun cppSetZnegProj(theCppPtr: Long) + private var myView: GLSurfaceView? = null //!< back reference to the View + private var myCppViewer: Long = 0 //!< pointer to c++ class instance + + //! Empty constructor. + init { + myView = theView // this makes cyclic dependency, but it is OK for JVM + if (OcctJniActivity.areNativeLoaded) { + myCppViewer = cppCreate(theScreenDensity) + } + } +} diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt new file mode 100644 index 0000000000..971aa198da --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt @@ -0,0 +1,250 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +package com.opencascade.jnisample + +import android.app.ActionBar +import android.content.Context +import android.graphics.PointF +import android.opengl.GLSurfaceView +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.RelativeLayout +import com.opencascade.jnisample.OcctJniLogger.postMessage +import com.opencascade.jnisample.OcctJniRenderer.TypeOfOrientation +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLDisplay + +//! OpenGL ES 2.0+ view. +//! Performs rendering in parallel thread. +internal class OcctJniView(theContext: Context, + theAttrs: AttributeSet?) : GLSurfaceView(theContext, theAttrs) { + //! Open file. + fun open(thePath: String) { + queueEvent { myRenderer!!.open(thePath) } + requestRender() + } + + //! Create OpenGL ES 2.0+ context + private class ContextFactory : EGLContextFactory { + override fun createContext(theEgl: EGL10, + theEglDisplay: EGLDisplay, + theEglConfig: EGLConfig): EGLContext { + // reset EGL errors stack + while (theEgl.eglGetError() != EGL10.EGL_SUCCESS) {} + val anAttribs = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE) + val aEglContext = theEgl.eglCreateContext(theEglDisplay, theEglConfig, EGL10.EGL_NO_CONTEXT, anAttribs) + var anError = theEgl.eglGetError() + while (anError != EGL10.EGL_SUCCESS) { + postMessage("Error: eglCreateContext() " + String.format("0x%x", anError)) + anError = theEgl.eglGetError() + } + return aEglContext + } + + override fun destroyContext(theEgl: EGL10, + theEglDisplay: EGLDisplay, + theEglContext: EGLContext) { + theEgl.eglDestroyContext(theEglDisplay, theEglContext) + } + + companion object { + private const val EGL_CONTEXT_CLIENT_VERSION = 0x3098 + } + } + + //! Search for RGB24 config with depth and stencil buffers + private class ConfigChooser : EGLConfigChooser { + //! Reset EGL errors stack + private fun popEglErrors(theEgl: EGL10) { + var anError = theEgl.eglGetError() + while (anError != EGL10.EGL_SUCCESS) { + postMessage("EGL Error: " + String.format("0x%x", anError)) + anError = theEgl.eglGetError() + } + } + + //! Auxiliary method to dump EGL configuration - for debugging purposes + private fun printConfig(theEgl: EGL10, + theEglDisplay: EGLDisplay, + theEglConfig: EGLConfig) { + val THE_ATTRIBS = intArrayOf( + EGL10.EGL_BUFFER_SIZE, EGL10.EGL_ALPHA_SIZE, EGL10.EGL_BLUE_SIZE, EGL10.EGL_GREEN_SIZE, EGL10.EGL_RED_SIZE, EGL10.EGL_DEPTH_SIZE, EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_HEIGHT, EGL10.EGL_MAX_PBUFFER_PIXELS, EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, EGL10.EGL_NATIVE_VISUAL_ID, EGL10.EGL_NATIVE_VISUAL_TYPE, + 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, EGL10.EGL_TRANSPARENT_RED_VALUE, EGL10.EGL_TRANSPARENT_GREEN_VALUE, EGL10.EGL_TRANSPARENT_BLUE_VALUE, + 0x3039, 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGB, EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, 0x303C, // EGL10.EGL_MIN_SWAP_INTERVAL, EGL10.EGL_MAX_SWAP_INTERVAL + EGL10.EGL_LUMINANCE_SIZE, EGL10.EGL_ALPHA_MASK_SIZE, + EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RENDERABLE_TYPE, + 0x3042 // EGL10.EGL_CONFORMANT + ) + val THE_NAMES = arrayOf( + "EGL_BUFFER_SIZE", "EGL_ALPHA_SIZE", "EGL_BLUE_SIZE", "EGL_GREEN_SIZE", "EGL_RED_SIZE", "EGL_DEPTH_SIZE", "EGL_STENCIL_SIZE", + "EGL_CONFIG_CAVEAT", + "EGL_CONFIG_ID", + "EGL_LEVEL", + "EGL_MAX_PBUFFER_HEIGHT", "EGL_MAX_PBUFFER_PIXELS", "EGL_MAX_PBUFFER_WIDTH", + "EGL_NATIVE_RENDERABLE", "EGL_NATIVE_VISUAL_ID", "EGL_NATIVE_VISUAL_TYPE", + "EGL_PRESERVED_RESOURCES", + "EGL_SAMPLES", "EGL_SAMPLE_BUFFERS", + "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", "EGL_TRANSPARENT_RED_VALUE", "EGL_TRANSPARENT_GREEN_VALUE", "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", "EGL_BIND_TO_TEXTURE_RGBA", + "EGL_MIN_SWAP_INTERVAL", "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", "EGL_ALPHA_MASK_SIZE", + "EGL_COLOR_BUFFER_TYPE", "EGL_RENDERABLE_TYPE", + "EGL_CONFORMANT" + ) + val aValue = IntArray(1) + for (anAttrIter in THE_ATTRIBS.indices) { + val anAttr = THE_ATTRIBS[anAttrIter] + val aName = THE_NAMES[anAttrIter] + if (theEgl.eglGetConfigAttrib(theEglDisplay, theEglConfig, anAttr, aValue)) { + postMessage(String.format(" %s: %d\n", aName, aValue[0])) + } else { + popEglErrors(theEgl) + } + } + } + + //! Interface implementation + override fun chooseConfig(theEgl: EGL10, + theEglDisplay: EGLDisplay): EGLConfig { + val EGL_OPENGL_ES2_BIT = 4 + val aCfgAttribs = intArrayOf( + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 0, + EGL10.EGL_DEPTH_SIZE, 24, + EGL10.EGL_STENCIL_SIZE, 8, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + ) + val aConfigs = arrayOfNulls(1) + val aNbConfigs = IntArray(1) + if (!theEgl.eglChooseConfig(theEglDisplay, aCfgAttribs, aConfigs, 1, aNbConfigs) + || aConfigs[0] == null) { + aCfgAttribs[4 * 2 + 1] = 16 // try config with smaller depth buffer + popEglErrors(theEgl) + if (!theEgl.eglChooseConfig(theEglDisplay, aCfgAttribs, aConfigs, 1, aNbConfigs) + || aConfigs[0] == null) { + postMessage("Error: eglChooseConfig() has failed!") + ///return null + } + } + + //printConfig (theEgl, theEglDisplay, aConfigs[0]); + return aConfigs[0]!! + } + } + + //! Callback to handle touch events + override fun onTouchEvent(theEvent: MotionEvent): Boolean { + val aMaskedAction = theEvent.actionMasked + when (aMaskedAction) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + val aPointerIndex = theEvent.actionIndex + val aPointerId = theEvent.getPointerId(aPointerIndex) + val aPnt = PointF(theEvent.getX(aPointerIndex), theEvent.getY(aPointerIndex)) + mySelectPoint = if (theEvent.pointerCount == 1) { + aPnt + } else { + null + } + queueEvent { myRenderer!!.onAddTouchPoint(aPointerId, aPnt.x, aPnt.y) } + } + MotionEvent.ACTION_MOVE -> { + val aNbPointers = theEvent.pointerCount + var aPntIter = 0 + while (aPntIter < aNbPointers) { + val aPointerId = theEvent.getPointerId(aPntIter) + val aPnt = PointF(theEvent.getX(aPntIter), theEvent.getY(aPntIter)) + queueEvent { myRenderer!!.onUpdateTouchPoint(aPointerId, aPnt.x, aPnt.y) } + ++aPntIter + } + if (mySelectPoint != null) { + val aTouchThreshold = 5.0f * myScreenDensity + val aPointerIndex = theEvent.actionIndex + val aDelta = PointF(theEvent.getX(aPointerIndex) - mySelectPoint!!.x, theEvent.getY(aPointerIndex) - mySelectPoint!!.y) + if (Math.abs(aDelta.x) > aTouchThreshold || Math.abs(aDelta.y) > aTouchThreshold) { + mySelectPoint = null + } + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> { + if (mySelectPoint != null) { + val aSelX = mySelectPoint!!.x + val aSelY = mySelectPoint!!.y + queueEvent { myRenderer!!.onSelectInViewer(aSelX, aSelY) } + mySelectPoint = null + } + val aPointerIndex = theEvent.actionIndex + val aPointerId = theEvent.getPointerId(aPointerIndex) + //val aPnt = PointF(theEvent.getX(aPointerIndex), theEvent.getY(aPointerIndex)) + queueEvent { myRenderer!!.onRemoveTouchPoint(aPointerId) } + } + } + requestRender() + return true + } + + //! Fit All + fun fitAll() { + queueEvent { myRenderer!!.fitAll() } + requestRender() + } + + //! Move camera + fun setProj(theProj: TypeOfOrientation?) { + queueEvent { myRenderer!!.setProj(theProj) } + requestRender() + } + + //! OCCT viewer + private var myRenderer: OcctJniRenderer? = null + private val mySelectId = -1 + private var mySelectPoint: PointF? = null + private var myScreenDensity = 1.0f + + // ! Default constructor. + init { + val aDispInfo = theContext.resources.displayMetrics + myScreenDensity = aDispInfo.density + preserveEGLContextOnPause = true + setEGLContextFactory(ContextFactory()) + setEGLConfigChooser(ConfigChooser()) + val aLParams = RelativeLayout.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.WRAP_CONTENT) + aLParams.addRule(RelativeLayout.ALIGN_TOP) + myRenderer = OcctJniRenderer(this, myScreenDensity) + setRenderer(myRenderer) + renderMode = RENDERMODE_WHEN_DIRTY // render on request to spare battery + } +} diff --git a/samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt b/samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000000..59e41e6e59 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.4.1) + +set(HEADER_FILES OcctJni_MsgPrinter.hxx OcctJni_Viewer.hxx) +set(SOURCE_FILES OcctJni_MsgPrinter.cxx OcctJni_Viewer.cxx) + +set (anOcctLibs + TKernel TKMath TKG2d TKG3d TKGeomBase TKBRep TKGeomAlgo TKTopAlgo TKShHealing TKMesh + # exchange + TKPrim TKBO TKBool TKFillet TKOffset + TKXSBase + TKSTL + TKIGES + TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP + # OCCT Visualization + TKService TKHLR TKV3d TKOpenGles +) + +set(aLibDeps "") + +# OCCT libraries +include_directories(${OCCT_ROOT}/inc) +foreach(anOcctLib ${anOcctLibs}) + add_library(lib_${anOcctLib} SHARED IMPORTED) + set_target_properties(lib_${anOcctLib} PROPERTIES IMPORTED_LOCATION ${OCCT_ROOT}/libs/${ANDROID_ABI}/lib${anOcctLib}.so) + list(APPEND aLibDeps lib_${anOcctLib}) +endforeach() + +# FreeType +add_library(lib_FreeType SHARED IMPORTED) +set_target_properties(lib_FreeType PROPERTIES IMPORTED_LOCATION ${FREETYPE_ROOT}/libs/${ANDROID_ABI}/libfreetype.so) +list(APPEND aLibDeps lib_FreeType) + +# FreeImage - uncomment, if OCCT was built with FreeImage +#add_library(lib_FreeImage SHARED IMPORTED) +#set_target_properties(lib_FreeImage PROPERTIES IMPORTED_LOCATION ${FREETYPE_ROOT}/libs/${ANDROID_ABI}/libfreeimage.so) +#list(APPEND aLibDeps lib_FreeImage) + +# system libraries +list(APPEND aLibDeps EGL GLESv2 log android) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -frtti -fexceptions -fpermissive") + +add_library(TKJniSample SHARED ${SOURCE_FILES}) +target_link_libraries(TKJniSample ${aLibDeps}) diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx new file mode 100644 index 0000000000..2934c97c9b --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx @@ -0,0 +1,79 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 "OcctJni_MsgPrinter.hxx" + +#include +#include + +#include + +IMPLEMENT_STANDARD_RTTIEXT(OcctJni_MsgPrinter, Message_Printer) + +// ======================================================================= +// function : OcctJni_MsgPrinter +// purpose : +// ======================================================================= +OcctJni_MsgPrinter::OcctJni_MsgPrinter (JNIEnv* theJEnv, + jobject theJObj) +: myJEnv (theJEnv), + myJObj (theJEnv->NewGlobalRef (theJObj)), + myJMet (NULL) +{ + jclass aJClass = theJEnv->GetObjectClass (theJObj); + myJMet = theJEnv->GetMethodID (aJClass, "postMessage", "(Ljava/lang/String;)V"); + if (myJMet == NULL) + { + __android_log_write (ANDROID_LOG_FATAL, "jniSample", "Broken initialization of OcctJni_MsgPrinter!"); + } +} + +// ======================================================================= +// function : ~OcctJni_MsgPrinter +// purpose : +// ======================================================================= +OcctJni_MsgPrinter::~OcctJni_MsgPrinter() +{ + //myJEnv->DeleteGlobalRef (myJObj); +} + +// ======================================================================= +// function : send +// purpose : +// ======================================================================= +void OcctJni_MsgPrinter::send (const TCollection_AsciiString& theString, + const Message_Gravity theGravity) const +{ + if (theGravity < myTraceLevel) + { + return; + } + + ///__android_log_write (ANDROID_LOG_DEBUG, "OcctJni_MsgPrinter", (TCollection_AsciiString(" @@ ") + theString).ToCString()); + if (myJMet == NULL) + { + return; + } + + jstring aJStr = myJEnv->NewStringUTF ((theString + "\n").ToCString()); + myJEnv->CallVoidMethod (myJObj, myJMet, aJStr); + myJEnv->DeleteLocalRef (aJStr); +} diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx new file mode 100644 index 0000000000..a52b03bcfd --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx @@ -0,0 +1,56 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 OcctJni_MsgPrinter_H +#define OcctJni_MsgPrinter_H + +#include + +#include + +// Class providing connection between messenger interfaces in C++ and Java layers. +class OcctJni_MsgPrinter : public Message_Printer +{ + DEFINE_STANDARD_RTTIEXT(OcctJni_MsgPrinter, Message_Printer) +public: + + //! Default constructor + OcctJni_MsgPrinter (JNIEnv* theJEnv, + jobject theJObj); + + //! Destructor. + ~OcctJni_MsgPrinter(); + +protected: + + //! Main printing method + virtual void send (const TCollection_AsciiString& theString, + const Message_Gravity theGravity) const override; + +private: + + JNIEnv* myJEnv; + jobject myJObj; + jmethodID myJMet; + +}; + +#endif // OcctJni_MsgPrinter_H diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx new file mode 100644 index 0000000000..59ec8e95f0 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx @@ -0,0 +1,718 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 "OcctJni_Viewer.hxx" +#include "OcctJni_MsgPrinter.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include + +// ======================================================================= +// function : OcctJni_Viewer +// purpose : +// ======================================================================= +OcctJni_Viewer::OcctJni_Viewer (float theDispDensity) +: myDevicePixelRatio (theDispDensity), + myIsJniMoreFrames (false) +{ + SetTouchToleranceScale (theDispDensity); +#ifndef NDEBUG + // Register printer for logging messages into global Android log. + // Should never be used in production (or specify higher gravity for logging only failures). + Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger(); + aMsgMgr->RemovePrinters (STANDARD_TYPE (Message_PrinterSystemLog)); + aMsgMgr->AddPrinter (new Message_PrinterSystemLog ("OcctJni_Viewer")); +#endif +} + +// ================================================================ +// Function : dumpGlInfo +// Purpose : +// ================================================================ +void OcctJni_Viewer::dumpGlInfo (bool theIsBasic) +{ + TColStd_IndexedDataMapOfStringString aGlCapsDict; + myView->DiagnosticInformation (aGlCapsDict, Graphic3d_DiagnosticInfo_Basic); //theIsBasic ? Graphic3d_DiagnosticInfo_Basic : Graphic3d_DiagnosticInfo_Complete); + if (theIsBasic) + { + TCollection_AsciiString aViewport; + aGlCapsDict.FindFromKey ("Viewport", aViewport); + aGlCapsDict.Clear(); + aGlCapsDict.Add ("Viewport", aViewport); + } + aGlCapsDict.Add ("Display scale", TCollection_AsciiString(myDevicePixelRatio)); + + // beautify output + { + TCollection_AsciiString* aGlVer = aGlCapsDict.ChangeSeek ("GLversion"); + TCollection_AsciiString* aGlslVer = aGlCapsDict.ChangeSeek ("GLSLversion"); + if (aGlVer != NULL + && aGlslVer != NULL) + { + *aGlVer = *aGlVer + " [GLSL: " + *aGlslVer + "]"; + aGlslVer->Clear(); + } + } + + TCollection_AsciiString anInfo; + for (TColStd_IndexedDataMapOfStringString::Iterator aValueIter (aGlCapsDict); aValueIter.More(); aValueIter.Next()) + { + if (!aValueIter.Value().IsEmpty()) + { + if (!anInfo.IsEmpty()) + { + anInfo += "\n"; + } + anInfo += aValueIter.Key() + ": " + aValueIter.Value(); + } + } + + Message::SendWarning (anInfo); +} + +// ======================================================================= +// function : init +// purpose : +// ======================================================================= +bool OcctJni_Viewer::init() +{ + EGLint aCfgId = 0; + int aWidth = 0, aHeight = 0; + EGLDisplay anEglDisplay = eglGetCurrentDisplay(); + EGLContext anEglContext = eglGetCurrentContext(); + EGLSurface anEglSurf = eglGetCurrentSurface (EGL_DRAW); + if (anEglDisplay == EGL_NO_DISPLAY + || anEglContext == EGL_NO_CONTEXT + || anEglSurf == EGL_NO_SURFACE) + { + Message::SendFail ("Error: No active EGL context!"); + release(); + return false; + } + + eglQuerySurface (anEglDisplay, anEglSurf, EGL_WIDTH, &aWidth); + eglQuerySurface (anEglDisplay, anEglSurf, EGL_HEIGHT, &aHeight); + eglQuerySurface (anEglDisplay, anEglSurf, EGL_CONFIG_ID, &aCfgId); + const EGLint aConfigAttribs[] = { EGL_CONFIG_ID, aCfgId, EGL_NONE }; + EGLint aNbConfigs = 0; + void* anEglConfig = NULL; + if (eglChooseConfig (anEglDisplay, aConfigAttribs, &anEglConfig, 1, &aNbConfigs) != EGL_TRUE) + { + Message::SendFail ("Error: EGL does not provide compatible configurations!"); + release(); + return false; + } + + if (!myViewer.IsNull()) + { + Handle(OpenGl_GraphicDriver) aDriver = Handle(OpenGl_GraphicDriver)::DownCast (myViewer->Driver()); + Handle(Aspect_NeutralWindow) aWindow = Handle(Aspect_NeutralWindow)::DownCast (myView->Window()); + if (!aDriver->InitEglContext (anEglDisplay, anEglContext, anEglConfig)) + { + Message::SendFail ("Error: OpenGl_GraphicDriver can not be initialized!"); + release(); + return false; + } + + aWindow->SetSize (aWidth, aHeight); + myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext); + dumpGlInfo (true); + return true; + } + + Handle(OpenGl_GraphicDriver) aDriver = new OpenGl_GraphicDriver (NULL, Standard_False); + aDriver->ChangeOptions().buffersNoSwap = true; + aDriver->ChangeOptions().buffersOpaqueAlpha = true; + aDriver->ChangeOptions().useSystemBuffer = false; + if (!aDriver->InitEglContext (anEglDisplay, anEglContext, anEglConfig)) + { + Message::SendFail ("Error: OpenGl_GraphicDriver can not be initialized!"); + release(); + return false; + } + + myTextStyle = new Prs3d_TextAspect(); + myTextStyle->SetFont (Font_NOF_ASCII_MONO); + myTextStyle->SetHeight (12); + myTextStyle->Aspect()->SetColor (Quantity_NOC_GRAY95); + myTextStyle->Aspect()->SetColorSubTitle (Quantity_NOC_BLACK); + myTextStyle->Aspect()->SetDisplayType (Aspect_TODT_SHADOW); + myTextStyle->Aspect()->SetTextFontAspect (Font_FA_Bold); + myTextStyle->Aspect()->SetTextZoomable (false); + myTextStyle->SetHorizontalJustification (Graphic3d_HTA_LEFT); + myTextStyle->SetVerticalJustification (Graphic3d_VTA_BOTTOM); + + // create viewer + myViewer = new V3d_Viewer (aDriver); + myViewer->SetDefaultBackgroundColor (Quantity_NOC_BLACK); + myViewer->SetDefaultLights(); + myViewer->SetLightOn(); + + // create AIS context + myContext = new AIS_InteractiveContext (myViewer); + myContext->SetPixelTolerance (int(myDevicePixelRatio * 6.0)); // increase tolerance and adjust to hi-dpi screens + myContext->SetDisplayMode (AIS_Shaded, false); + + Handle(Aspect_NeutralWindow) aWindow = new Aspect_NeutralWindow(); + aWindow->SetSize (aWidth, aHeight); + myView = myViewer->CreateView(); + myView->SetImmediateUpdate (false); + myView->ChangeRenderingParams().Resolution = (unsigned int )(96.0 * myDevicePixelRatio + 0.5); + myView->ChangeRenderingParams().ToShowStats = true; + myView->ChangeRenderingParams().CollectedStats = (Graphic3d_RenderingParams::PerfCounters ) (Graphic3d_RenderingParams::PerfCounters_FrameRate | Graphic3d_RenderingParams::PerfCounters_Triangles); + myView->ChangeRenderingParams().StatsTextAspect = myTextStyle->Aspect(); + myView->ChangeRenderingParams().StatsTextHeight = (int )myTextStyle->Height(); + + myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext); + dumpGlInfo (false); + //myView->TriedronDisplay (Aspect_TOTP_RIGHT_LOWER, Quantity_NOC_WHITE, 0.08 * myDevicePixelRatio, V3d_ZBUFFER); + + initContent(); + return true; +} + +// ======================================================================= +// function : release +// purpose : +// ======================================================================= +void OcctJni_Viewer::release() +{ + myContext.Nullify(); + myView.Nullify(); + myViewer.Nullify(); +} + +// ======================================================================= +// function : resize +// purpose : +// ======================================================================= +void OcctJni_Viewer::resize (int theWidth, + int theHeight) +{ + if (myContext.IsNull()) + { + Message::SendFail ("Resize failed - view is unavailable"); + return; + } + + Handle(OpenGl_GraphicDriver) aDriver = Handle(OpenGl_GraphicDriver)::DownCast (myViewer->Driver()); + Handle(Aspect_NeutralWindow) aWindow = Handle(Aspect_NeutralWindow)::DownCast (myView->Window()); + aWindow->SetSize (theWidth, theHeight); + //myView->MustBeResized(); // can be used instead of SetWindow() when EGLsurface has not been changed + + EGLContext anEglContext = eglGetCurrentContext(); + myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext); + dumpGlInfo (true); +} + +// ======================================================================= +// function : initContent +// purpose : +// ======================================================================= +void OcctJni_Viewer::initContent() +{ + myContext->RemoveAll (Standard_False); + + if (myViewCube.IsNull()) + { + myViewCube = new AIS_ViewCube(); + { + // setup view cube size + static const double THE_CUBE_SIZE = 60.0; + myViewCube->SetSize (myDevicePixelRatio * THE_CUBE_SIZE, false); + myViewCube->SetBoxFacetExtension (myViewCube->Size() * 0.15); + myViewCube->SetAxesPadding (myViewCube->Size() * 0.10); + myViewCube->SetFontHeight (THE_CUBE_SIZE * 0.16); + } + // presentation parameters + myViewCube->SetTransformPersistence (new Graphic3d_TransformPers (Graphic3d_TMF_TriedronPers, Aspect_TOTP_RIGHT_LOWER, Graphic3d_Vec2i (200, 200))); + myViewCube->Attributes()->SetDatumAspect (new Prs3d_DatumAspect()); + myViewCube->Attributes()->DatumAspect()->SetTextAspect (myTextStyle); + // animation parameters + myViewCube->SetViewAnimation (myViewAnimation); + myViewCube->SetFixedAnimationLoop (false); + myViewCube->SetAutoStartAnimation (true); + } + myContext->Display (myViewCube, false); + + OSD_Timer aTimer; + aTimer.Start(); + if (!myShape.IsNull()) + { + Handle(AIS_Shape) aShapePrs = new AIS_Shape (myShape); + myContext->Display (aShapePrs, Standard_False); + } + else + { + BRepPrimAPI_MakeBox aBuilder (1.0, 2.0, 3.0); + Handle(AIS_Shape) aShapePrs = new AIS_Shape (aBuilder.Shape()); + myContext->Display (aShapePrs, Standard_False); + } + myView->FitAll(); + + aTimer.Stop(); + Message::SendInfo (TCollection_AsciiString() + "Presentation computed in " + aTimer.ElapsedTime() + " seconds"); +} + +//! Load shape from IGES file +static TopoDS_Shape loadIGES (const TCollection_AsciiString& thePath) +{ + TopoDS_Shape aShape; + IGESControl_Reader aReader; + IFSelect_ReturnStatus aReadStatus = IFSelect_RetFail; + try + { + aReadStatus = aReader.ReadFile (thePath.ToCString()); + } + catch (Standard_Failure) + { + Message::SendFail ("Error: IGES reader, computation error"); + return aShape; + } + + if (aReadStatus != IFSelect_RetDone) + { + Message::SendFail ("Error: IGES reader, bad file format"); + return aShape; + } + + // now perform the translation + aReader.TransferRoots(); + if (aReader.NbShapes() <= 0) + { + Handle(XSControl_WorkSession) aWorkSession = new XSControl_WorkSession(); + aWorkSession->SelectNorm ("IGES"); + aReader.SetWS (aWorkSession, Standard_True); + aReader.SetReadVisible (Standard_False); + aReader.TransferRoots(); + } + if (aReader.NbShapes() <= 0) + { + Message::SendFail ("Error: IGES reader, no shapes has been found"); + return aShape; + } + return aReader.OneShape(); +} + +//! Load shape from STEP file +static TopoDS_Shape loadSTEP (const TCollection_AsciiString& thePath) +{ + STEPControl_Reader aReader; + IFSelect_ReturnStatus aReadStatus = IFSelect_RetFail; + try + { + aReadStatus = aReader.ReadFile (thePath.ToCString()); + } + catch (Standard_Failure) + { + Message::SendFail ("Error: STEP reader, computation error"); + return TopoDS_Shape(); + } + + if (aReadStatus != IFSelect_RetDone) + { + Message::SendFail ("Error: STEP reader, bad file format"); + return TopoDS_Shape(); + } + else if (aReader.NbRootsForTransfer() <= 0) + { + Message::SendFail ("Error: STEP reader, shape is empty"); + return TopoDS_Shape(); + } + + // now perform the translation + aReader.TransferRoots(); + return aReader.OneShape(); +} + +//! Load shape from STL file +static TopoDS_Shape loadSTL (const TCollection_AsciiString& thePath) +{ + Handle(Poly_Triangulation) aTri = RWStl::ReadFile (thePath.ToCString()); + TopoDS_Face aFace; + BRep_Builder().MakeFace (aFace, aTri); + return aFace; +} + +// ======================================================================= +// function : open +// purpose : +// ======================================================================= +bool OcctJni_Viewer::open (const TCollection_AsciiString& thePath) +{ + myShape.Nullify(); + if (!myContext.IsNull()) + { + myContext->RemoveAll (Standard_False); + if (!myViewCube.IsNull()) + { + myContext->Display (myViewCube, false); + } + } + if (thePath.IsEmpty()) + { + return false; + } + + OSD_Timer aTimer; + aTimer.Start(); + TCollection_AsciiString aFileName, aFormatStr; + OSD_Path::FileNameAndExtension (thePath, aFileName, aFormatStr); + aFormatStr.LowerCase(); + + TopoDS_Shape aShape; + if (aFormatStr == "stp" + || aFormatStr == "step") + { + aShape = loadSTEP (thePath); + } + else if (aFormatStr == "igs" + || aFormatStr == "iges") + { + aShape = loadIGES (thePath); + } + else if (aFormatStr == "stl") + { + aShape = loadSTL (thePath); + } + else + // if (aFormatStr == "brep" + // || aFormatStr == "rle") + { + BRep_Builder aBuilder; + if (!BRepTools::Read (aShape, thePath.ToCString(), aBuilder)) + { + Message::SendInfo (TCollection_AsciiString() + "Error: file '" + thePath + "' can not be opened"); + return false; + } + } + if (aShape.IsNull()) + { + return false; + } + aTimer.Stop(); + Message::SendInfo (TCollection_AsciiString() + "File '" + thePath + "' loaded in " + aTimer.ElapsedTime() + " seconds"); + + myShape = aShape; + if (myContext.IsNull()) + { + return true; + } + + aTimer.Reset(); + aTimer.Start(); + + Handle(AIS_Shape) aShapePrs = new AIS_Shape (aShape); + myContext->Display (aShapePrs, Standard_False); + myView->FitAll(); + + aTimer.Stop(); + Message::SendInfo (TCollection_AsciiString() + "Presentation computed in " + aTimer.ElapsedTime() + " seconds"); + return true; +} + +// ======================================================================= +// function : saveSnapshot +// purpose : +// ======================================================================= +bool OcctJni_Viewer::saveSnapshot (const TCollection_AsciiString& thePath, + int theWidth, + int theHeight) +{ + if (myContext.IsNull() + || thePath.IsEmpty()) + { + Message::SendFail ("Image dump failed - view is unavailable"); + return false; + } + + if (theWidth < 1 + || theHeight < 1) + { + myView->Window()->Size (theWidth, theHeight); + } + if (theWidth < 1 + || theHeight < 1) + { + Message::SendFail ("Image dump failed - view is unavailable"); + return false; + } + + Image_AlienPixMap anImage; + if (!anImage.InitTrash (Image_Format_BGRA, theWidth, theHeight)) + { + Message::SendFail (TCollection_AsciiString() + "RGBA image " + theWidth + "x" + theHeight + " allocation failed"); + return false; + } + + if (!myView->ToPixMap (anImage, theWidth, theHeight, Graphic3d_BT_RGBA)) + { + Message::SendFail (TCollection_AsciiString() + "View dump to the image " + theWidth + "x" + theHeight + " failed"); + } + if (!anImage.Save (thePath)) + { + Message::SendFail (TCollection_AsciiString() + "Image saving to path '" + thePath + "' failed"); + return false; + } + Message::SendInfo (TCollection_AsciiString() + "View " + theWidth + "x" + theHeight + " dumped to image '" + thePath + "'"); + return true; +} + +// ================================================================ +// Function : handleViewRedraw +// Purpose : +// ================================================================ +void OcctJni_Viewer::handleViewRedraw (const Handle(AIS_InteractiveContext)& theCtx, + const Handle(V3d_View)& theView) +{ + AIS_ViewController::handleViewRedraw (theCtx, theView); + myIsJniMoreFrames = myToAskNextFrame; +} + +// ======================================================================= +// function : redraw +// purpose : +// ======================================================================= +bool OcctJni_Viewer::redraw() +{ + if (myView.IsNull()) + { + return false; + } + + // handle user input + myIsJniMoreFrames = false; + myView->InvalidateImmediate(); + FlushViewEvents (myContext, myView, true); + return myIsJniMoreFrames; +} + +// ======================================================================= +// function : fitAll +// purpose : +// ======================================================================= +void OcctJni_Viewer::fitAll() +{ + if (myView.IsNull()) + { + return; + } + + myView->FitAll (0.01, Standard_False); + myView->Invalidate(); +} + +#define jexp extern "C" JNIEXPORT + +jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppCreate (JNIEnv* theEnv, + jobject theObj, + jfloat theDispDensity) +{ + return jlong(new OcctJni_Viewer (theDispDensity)); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppDestroy (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + delete (OcctJni_Viewer* )theCppPtr; + + Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger(); + aMsgMgr->RemovePrinters (STANDARD_TYPE (OcctJni_MsgPrinter)); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRelease (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->release(); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppInit (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger(); + aMsgMgr->RemovePrinters (STANDARD_TYPE (OcctJni_MsgPrinter)); + aMsgMgr->AddPrinter (new OcctJni_MsgPrinter (theEnv, theObj)); + ((OcctJni_Viewer* )theCppPtr)->init(); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppResize (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jint theWidth, + jint theHeight) +{ + ((OcctJni_Viewer* )theCppPtr)->resize (theWidth, theHeight); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppOpen (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jstring thePath) +{ + const char* aPathPtr = theEnv->GetStringUTFChars (thePath, 0); + const TCollection_AsciiString aPath (aPathPtr); + theEnv->ReleaseStringUTFChars (thePath, aPathPtr); + ((OcctJni_Viewer* )theCppPtr)->open (aPath); +} + +jexp jboolean JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRedraw (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + return ((OcctJni_Viewer* )theCppPtr)->redraw() ? JNI_TRUE : JNI_FALSE; +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetAxoProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_XposYnegZpos); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetXposProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Xpos); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetYposProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Ypos); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetZposProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Zpos); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetXnegProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Xneg); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetYnegProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Yneg); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetZnegProj (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Zneg); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppFitAll (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr) +{ + ((OcctJni_Viewer* )theCppPtr)->fitAll(); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppAddTouchPoint (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jint theId, + jfloat theX, + jfloat theY) +{ + ((OcctJni_Viewer* )theCppPtr)->AddTouchPoint (theId, Graphic3d_Vec2d (theX, theY)); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppUpdateTouchPoint (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jint theId, + jfloat theX, + jfloat theY) +{ + ((OcctJni_Viewer* )theCppPtr)->UpdateTouchPoint (theId, Graphic3d_Vec2d (theX, theY)); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRemoveTouchPoint (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jint theId) +{ + ((OcctJni_Viewer* )theCppPtr)->RemoveTouchPoint (theId); +} + +jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSelectInViewer (JNIEnv* theEnv, + jobject theObj, + jlong theCppPtr, + jfloat theX, + jfloat theY) +{ + ((OcctJni_Viewer* )theCppPtr)->SelectInViewer (Graphic3d_Vec2i ((int )theX, (int )theY)); +} + +jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMajorVersion (JNIEnv* theEnv, + jobject theObj) +{ + return OCC_VERSION_MAJOR; +} + +jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMinorVersion (JNIEnv* theEnv, + jobject theObj) +{ + return OCC_VERSION_MINOR; +} + +jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMicroVersion (JNIEnv* theEnv, + jobject theObj) +{ + return OCC_VERSION_MAINTENANCE; +} diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx new file mode 100644 index 0000000000..f56e86eb49 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx @@ -0,0 +1,99 @@ +// Copyright (c) 2014-2021 OPEN CASCADE SAS +// +// This file is part of the examples of the Open CASCADE Technology software library. +// +// 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 + +class AIS_ViewCube; + +//! Main C++ back-end for activity. +class OcctJni_Viewer : public AIS_ViewController +{ + +public: + + //! Empty constructor + OcctJni_Viewer (float theDispDensity); + + //! Initialize the viewer + bool init(); + + //! Release the viewer + void release(); + + //! Resize the viewer + void resize (int theWidth, + int theHeight); + + //! Open CAD file + bool open (const TCollection_AsciiString& thePath); + + //! Take snapshot + bool saveSnapshot (const TCollection_AsciiString& thePath, + int theWidth = 0, + int theHeight = 0); + + //! Viewer update. + //! Returns TRUE if more frames should be requested. + bool redraw(); + + //! Move camera + void setProj (V3d_TypeOfOrientation theProj) + { + if (myView.IsNull()) + { + return; + } + + myView->SetProj (theProj); + myView->Invalidate(); + } + + //! Fit All. + void fitAll(); + +protected: + + //! Reset viewer content. + void initContent(); + + //! Print information about OpenGL ES context. + void dumpGlInfo (bool theIsBasic); + + //! Handle redraw. + virtual void handleViewRedraw (const Handle(AIS_InteractiveContext)& theCtx, + const Handle(V3d_View)& theView) override; + +protected: + + Handle(V3d_Viewer) myViewer; + Handle(V3d_View) myView; + Handle(AIS_InteractiveContext) myContext; + Handle(Prs3d_TextAspect) myTextStyle; //!< text style for OSD elements + Handle(AIS_ViewCube) myViewCube; //!< view cube object + TopoDS_Shape myShape; + float myDevicePixelRatio; //!< device pixel ratio for handling high DPI displays + bool myIsJniMoreFrames; //!< need more frame flag + +}; diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png new file mode 100644 index 0000000000000000000000000000000000000000..0125c4aaf8867b36f628ab01923c6514b3397b0a GIT binary patch literal 1410 zcmV-|1%3L7P)N2bZe?^J zG%heMGmPe!Pyhf007*naR9Hv7nBR*VRTRgoLe*DY+9We`=SR}rCRwvdCX-AunaRv# zerA)+ZklBM14;#bP#6&3WQpr94|AY!5TP^dppp;%fgb_KWH zO^#>QTYU5-w;vb?xnDTkJLjJBJ#%OQJdi1=57OWFk5svFSZ+F{T+6wXs~fYNssUYM z3)gUt%T|18*^cLV%Y%H&f)&=mg|)^bf*GHd+>O0uZv%?nIutt|ENcz6fl0OX7;g_g z=G{%m`kRmsHXv{NkX7qDRKfCQ{c)x3y`^*qcO`#_K{r5j zW8ii3Ywig(M`epgQjL?@PPCK7t0Q8Gl)OHO9e|;`SBgi`OOwo;Zse}ze!#Jb3@t%?#F*XG`X$)4d zORBYJsbuj86}2Rq&P5sDsTAP7O@jIpkXl`~cUNraFUW52r<`PQFv7&b9lK*7TIX{r zVS{9}FDrgDm%<@7gfZ~m1bAzBm#Y}hQ3)Gl)7n#l7{qAvpc8T338b3+@6sh*r;^1Z zsny?Py8~j_19C60GoBsQ;#bqF^(87>Jc6cP7aeCNhU*v@?{6>xVohclwfAg z;z?`OcX-FSAx8a!z!GM&Y)ygG2A>K=hDV*MlwaaIonOQVTOT%j*`K1tL66niyiC@N z5tT7vkTKd~4c%k1va=)ufumbEKHpkJPpEob3TY`82qriev^B-P95 zfvmSaq_V}cw32`LXt_2bmVYy85@L7)IBbm)&PuY;`GLyVaI{!9xVm{!bk^rdz21bn0YC6%y2DAm_U%e^R}3lWoIl33fI z4}xL;!IkSzQyCkCnt4vNqy3~|Nc|K4+S^Vr7iz|PRKfQ9a+A!PY zn(-!;up`@cz9Altsd%SJG{F*z?_76(q|5BcYx-H-?}tO0!I?^$U_aoFBu$h5tUVv7MC@_?Gu#&guV^$0=jgjbPPsA+I%l zqRZ?^SB+fG^v@UUaHnXx^MY>NpO2`i)#^i}s&|x{{(82e-JpN}4~Gu@0}VUi&tq&i Qn*aa+07*qoM6N<$f(pc)N2bZe?^J zG%heMGmPe!Pyhe~t4TybR9HvtmD`IObri=}1uw6>H@}%ocbjC*COesAE}8p1 zm%XIP)<0mWU?0>6TM!C92)_9s_#{4vV4;c%`chEPO8X#Uq4-d!7gQ*gUc|28Zg(?3 z&t%6JUkatR1@nQ!OftVY=X-v?bI#06nKEU{lqq+MeU*|7mwv8ZdnGR|y~jx66)Z0O zw)gY_Wp=*w*lfA{#Oz}2_`arex}dc$(x&+fZQEB_+a7b4NjTlu!CLzQ+mc^m>S8r} z_5QQJQq%U=8iReJd?Ke9Hz>;^$d3sdb-{Xs4dEJg2p`0R_oIY!f<>gewlFL8o|+kplB_#RY+!);BbX-0X+A5gm>Z{ z-taFK73Vw_wGFN&Kb5)u?`AslcE`I@@?-O+%%qL>WWTPrAM=ncaMVDK9ZO1 z&*`@RD{Do&7y`2(A{E4jhrm*CBZ9rdgmb_wFk_t{V>G7h;h1tqg!UjIe+Y@~8Rrim zku_5ERtfL+Z(%FGhT7iulw!Y@Yv~*LhN5TT@0^k(pD4ENZx}n;V*Fl$pbXM-V4*Y^ z*l`MWlL8TFB4`9+*0*_Uc!5(_FQ7*Mvx3-pcfSO|?(ZeuP(@4+JB+BG}!V$m=Bq za8Ac-gzZ5I^bQf+0nw9yJrL3*oDGd?gnx00dy*9m1ubd=rXtIQN;98dXwK%VvP{*r z7m#FrfmQc9Zv5=@BCeW()GBUbnXnSRg_J;`*9wro64oai|jX6mGoa1N(0>Xr`Bf^^A zHm8|CaI*Rls*6W6m$wBiHJ;~X?G)FxF0-b;%Q`Xm>`#0nh(STDJe+bafY`x0p@a2= zatBvvtN#h3$H%CaeK1#RrM>bHIFn65_fHFU?@#QkB($5B#R$YdGcgoFZNpSaCkS>r z1erm?Xx8s^tNj(-P>$_Mx-;lS(FPx%(z5S~}E(_lBcpknnC8&Z5Xe=2X5t`aKTRt;WmNl|qIHY2FWL|fPpxN6Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000A7NklS}Q%9gD1`dS{5^lY5OHa$Oakh zl><}leYcX)pz=nNdn3D!R@VyC&Kiy8727wW4lwL}({RL-o{Ves(0zOO&5F0t1uPi}>b<)|=K-g;7 zB8&jxnS3*%abS#T1g|&;YCtXS^Uz=xxCB`o!c4hFhXs*^mBpj3u9Sb5Maj#)PP#t z=X5|sNVwZi2ZLvb$c{hI9?3gH6xsOmW)ivld)#T|7g}vS7z8sH z*=%a^&Nx2q=y5IH{Xm_3Wadbl9!uUCM}wN65n=G`;Fz#DN8TC7;TClfHP)X~Cry3( zTm&}(VzC?=)C6A=T98{!_=ltcC~r*Ps^ERO?)r YzrD-?<2F(YPXGV_07*qoM6N<$f`H1phyVZp literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d27ba82c09f3fa1357a7501c50058a10524fdc8a GIT binary patch literal 2591 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 literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png new file mode 100644 index 0000000000000000000000000000000000000000..a3dc8cc2092e136070180accd996700d34e35318 GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL8%hgh?3y^w370~qEv=}#LT=BJwMkFg)(D3 zQ$0figD*u3fyQcix;TbJxF<`nE=~~XX`0A)WbYI>Sd{IUQ0|p z0=pX1@|?;T1aAnxQVD5zc;L%2(*Vsbh6`(^8W?vTIbyY;u)tbiM%g{4mybgZqt5QXN^3W|R9$wCywb2RUrmvv;hWp3V^evY*}axr zyDzFW$71R=35m12FK`tH8J%o+5Xc_XN2bPDNB8 zb~7$DE-^4L^m3s900R+8L_t(oN6nbcYZE~jhm%;XwHPn?Xg1l3P{krukRalro~pHn z9=&Aqonj3s>9$SUrj5mqwzjr{e}DxA{{z8;dKJWrDBe^Q8U(3c6pA11`kQnXvy%bS z*q!VPPj+~dclX(k*_n(@)3lw!&dL2Z++>3>oGAHlR3w5GZL=XF1af>QH~?C~Zm z3xYsw{Hr%TcZ%-9+^9#eJ)~|*$>Lp4dfq{IV_wuJIIPLa$S2>xOjJr0&+HH_RLL0i z3X-cRN5<9z(foIYZ=75$9HC0a;0el+Q7~2}K+)TJo`v__zj93bC{;2BD~Mb7#vjWE zeTjmE-s%@cAew8&@tYVNRf7Kb(yFV7|fh83h$p^T)>L+b&)0z!`lg=Ng5g z{`AKtrVWC!qJD(nc_}&7OJu8x1cDK(A7+FXpi-%<4kSvXnB{I@(ph?`XXX)B02(;00000NkvXXu0mjfxi+FH literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png new file mode 100644 index 0000000000000000000000000000000000000000..6069ce38931d2f2a277d74bef58c431ff9d7e0dc GIT binary patch literal 1415 zcmV;21$g?2P)N2bZe?^J zG%heMGmPe!Pyhf01xZ9fR9Hu~n9GY>RUF5wf{(2ZZIXNMx#!%ZGfgsPlHANPb`VanAX@P7eH6iVf|W($e2VP2J9|HXmX?^QEF2v}$_uiWBda%+a`;N~^+Te864z7wiqFJlLf2XhKm8>D3Q^6KeX1N%$bOjQ5cp!Er+&86Bf? za+JzWvM;sF(@eq#q27L0GMqnf1Ua4UI@zQc(hT*y3sSAU#$U@H=!sCu6RP_ z5O!_HyOP>_fl2rvwz?-}JGg_w5fviXbp*~K+J&}x;ZU`1Fc}|+VgHDsX$T=WhMZPl z*T@R5AF8(|ODp9^m~8P7z4sAu2NZ=})H5^h4x>v4i-CSC$zrS3FNJZQJHP@)W!eom_ zs_HswjXxt6IE5V}C?;|;g6YJ)me-o^G1=lNRNMK2I(QdZTeB>{?m3Q!kkjr=(o}v! zLVmTqbVy5I7u@MgiZjPh!QufWj!m)J{GG`bPrjz>x$fvY!QXU z4?@FuUbHsuib3Xh3c0)i6)c%!qkoA>_<%I~3*tfL%BmOC>uf+EUi7Ej(O+Z|KH#2z zLxE_<;21;6B=E|RP4|M-P-{%Gcu=$ToMOQklsSc!H3`K__oR-&w_GZ^ai-{|GaSGX z!;oJJ;eM^~d!epXnPl;ZYpP57&J_YpL56Wxf!25!rjTOSyT~MbfI;9>*$(${cBgQ} z*{of(lU=MD$C+gD2v4ddQ8&Mqy%Yj!9qt#7*dO0j`oTM-u0O$Ki$`d5PDp+47LLZ0 zq)@3c=T3SZx2!)DM1;8eNk~_<+0K4GckrV6>HBiQ#Zx z=(?{7HSJ+0SvAeMXY(uZSHgM%#162;X96KH!G>;j-!PufX`Kz!3}36a;^A6?bfvN%&Ay+q-2a zq-6-EB9utcgXN4rLCxOdOu~m{twXav*sAbWS`MI6dFy+UnVyu^I*&35A68V2mU|X0 zTLE2m(mAoxi$cde!pwXrf%8(^n3a0&MbdR^xq63RrTRAtHT6OGc;NoG|I2{`{{YWU V;LmX$iEaP@002ovPDHLkV1mJ`uTB5} literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png new file mode 100644 index 0000000000000000000000000000000000000000..c0898e6d6a4835fc8614573dccdb71b564f8aa5c GIT binary patch literal 1322 zcmV+_1=aeAP)N2bZe?^J zG%heMGmPe!Pyhe~s7XXYR9HvtmCK7;bri=(g{oT}W|G|3@7|;{O)_SZ+}z|oa-X@+ zAU5HpHE(-O53dNzdVn=YMGjosU zX6|08t?eQoI3)b?`<>tSJbou*BSwrEF=9l@v?#%p)PbVd-X;z4gKYVMLJ+01sPRHs z>in239a2K^Wz~SH=|b7^U`p@p6UF?764lLY%YB0u)z?|sxKh&W-D$ly)Hq`}P?0sr zwqO8l?VxP9P<01T_UB>Rn}?D+f52P)YeeaN!MD2SNke`m+kVd>_09(0(%vGn^MhdZ z_k`XYlzK5t_XbcwjWbRNRozL74e9#uKuNVPO<7Sg<<3LVTLi&B4t%%<#9slyU4)Vo zCq*OJCW`YLD|IhWjm{?Zlvo-bzDrQ`_Onb&dygpgPlE36l>k0V-IBg1-(-unHu^!~Oyg%m8Zz z3Db3M(X##(-BLcGXGJ^Ha7UnzlXR#1C8Jrtv!3a(FteS<*rxgf>6oIRd2b44bd&AH zNggv&h!%lK1L$Kl@HgUZ}S7OjVZI zS#6mU4TV>FX9Oeoi?{oD9xLj_z^1-Z-|=za;^o5taE*0i%=X|`$q2Uu)p?(j^b>4D zsb_NknPXzx7F6RS+V*c!ZnRH(gMHSUOStqx53_~ug-iR?#h|e51S~V}tN?8++|DWS zx06!zT0!i;!?xx=<<-T@g0*tF=&tSZxQ|WS)Wgd9OPJwh;IPVExCm^p0E`zWj2-S2 zyY|;*Nj{y)xpVR@MbCBg4N9?JWxMWo#ENdN)Eg{9#c4yO%g z2HULaTx44M8LB47g-WZEU3u@|>++LC)HaHWb4f6~tAZ2l@#&jL+Y8%Z*bYO($B#Cg zWcV;y{EuOW(=5d5csHoFevuK?7noXmHgo;|L)E1!)6q_Gx_yy%{67RQOavdNRM1Bo zU@$RS`d}lVEEjT$adSeHPfylc%ahf*mbtnAFb%QBb>tU`*1gQ@#vX5(*dq1;ZQIvr z)BL%hb+1gcrE}x6^;6@O%H!kHn(=R$gkpn`_ix#-+K5YFR$~b;O7f gBSwt)uZ)fT4YDub&rYOub^rhX07*qoM6N<$f_wIrhX4Qo literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png new file mode 100644 index 0000000000000000000000000000000000000000..77ab199ab5decb6fb87e47ab3ca62b3f51f5c57a GIT binary patch literal 1716 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 FV1mREeRco< literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png new file mode 100644 index 0000000000000000000000000000000000000000..b93d4d3e099ad3cb90324f88ffb2cd4be2494735 GIT binary patch literal 1604 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 N002ovPDHLkV1hP3QUd@0 literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png new file mode 100644 index 0000000000000000000000000000000000000000..8cdb338b4ce8e40aefdb016c0bfd3ba57fee6855 GIT binary patch literal 1644 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 RU6ud<002ovPDHLkV1gkYd;tIe literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4b86dbf5e952b98811db056f7fe155288f8ff320 GIT binary patch literal 1609 zcmV-P2DbT$P)U2!JO++!G zMn&-e0mVdvq9Z8L1XLhuRN_fA0dLf3JpMpH1P@NxT!dwUatqzjt!rt!6|$?{;~(WH zHH;l4jY+@B^T+S~J=gbrpZ9&A-%nJP|1%O|oxng5ClG7I3B($40sYPlG4&bo6E&tyLao|Q4gl7Q5RtB!Ug!cI?&@XyBsG_ z6zy1pqCO)b_cjO6{)jsm2}z)sRnY-MBuc?XjYcoSXRoXpDvRTiHV`H z9e>gN>g!t9;K1(n@?}FKKLDwvC9Hp8o|w~yf;AtKlTTl=4GwTN!@)z)*+uaFNhCU( z;j!6Tcla>g!qH^ibC;MeDIxKZSz5bm505yVy7qMbdb|hBX1Q%_DNC0vkr5&%$2&V= z(^i5THmke2nc(9EJAw(8fKv&qB|&^)S>3nVE1wAtWV*-oN%1?$_VZ zTUGUU5iyN0F{8lBqi-#iQS%mPL)CVsx?H-x&jbKW!)3~?6L{mr`I6nEBH(g|B+_Mf zHw+pC`R9Yd@L#)UUr@*LFBzk%jVBTl8ChUu)o1TZ$-ISHw{07fJ*~PW()Nt^fYYU4 z1Jkxq$h;m4aec}5CnYTW2)L9BI6%_N-MwlMDiw1(T_#sLRD=K z*XvcEQd_UjE&W*6Y}!IccMzg2?d>Sg6&Y7mT`etCc|7p+6Ov=GFgsGG*Nv9FjtAzy ztW|sVqQO9DCZi>tq0}!!!UIaiiJYb$hi&=_gk=WbL) zMqX6R<`3SG^V8F!L!k8tq!~{g3j{(NAHeB?XBMe_$8H|<`*p)9`Shoq7LoiyD{Gf7 zmi+8t{Y^V`T79*Gz{7Q%kw3IwF1as~c6-10r(m z!?X40Hy4U6Z&crX1cNYlkv3J;ur(<`ZkjlOA;;fU{GqPu9a8^=ZFKXb*QcxVM#=Yk_A~aDagx2U60h6Cx??eV zOdg{2MJ6Jnb4D^JF_H1ll_%sGkIiMM*E;oL+qIwW_deYK53Tr7m)v`soG1rLfbdif+O^H+=kv(#3{|ZGYWf;GDg;Et zWXWdE4cEx@xsS?_q%g0l5WYcYOp+lL+x1NmX#@7EYW;v>5440tBt1Km{m++6MrpD1 zYC>9%!0L^9WMhT4)$HbWRjuf6`~k0ksY+{>znY~9UQec=uw}a3SjR*;z#Ybiijiumz%PF(Bb61J|cEW_NmOA`R)=pNS@APzipX!Ai2FM1yckdZLTTltr#DT-Jxb>I& z3bw6u&k-ViF5_tOM87wQ_3(-o5`l8`3~o}kzivgx_lK_)Rx|1njyB=tK z5|l(EDwFO2dLEKnKQd@Fpx})6L7im%QFg|27$WSycE_N@Z}2~fwP2!ZKmY#_&7!6S z(f;`Xo%F?Cdq>TO{-YNdHrl_`B;i0ehKQ^-77(J*MCtIGlPcvSUB4d7+%MoQLmmxnoLiW1iQy3@^}2QZaW<=O5kHh zu3jX6h9<)x+6Wt0=#7QOlZE%OB7>6M)ytGNq0|F>{LdzwtnZWD-{HAj->s_AYbU1O zddMLcV%*5BC^uRBtpY#_yqt2FeWFj0dswzrHICYJ5wRud@EO(Yd<*Qp8gkC8}wWb>0$O2&n{TbWfDJgg@0lG)SZIwVFu4w#^q&4S^8a zQ3+s}B~h8XO@(3DKva0ZRXzFnn3;KM5|Vam#Et^ z@Zd%>a_O%P!%Tgqf%!fKx?ZHx?U2k?)^^pNzU#1fg=G!zW_iqXe#Gd~j^Y!Gdquws z*p{WwON@~9%zkMkwf*K(nTqyMJGy->G%;sCSc#r^a?OzYgj-;-R+Os__P!6KGG^^F z_zs_!{b*T%1{%iYx1D9jN^&yn-gK2{e^jI3;xAjdT76HxR)$pPf=?8Dbw4xp?UC)= z%?QiIUN>!dor~o2;E0w7mZir^m83ZnzSP_GQo%@B;Mxx{~H zwY?|6Tf|d|Ly20k@TM`}9tSVkLQ!64{ALNq7gcU}zLh=$1ZL>6K_QN)Z`RU~JoPm)zQFFXXdo z+A)~^qZ+|Z=9u%;x+*8$`Z!$PU%5D+8!-9%CICenFl-X+z(skfm+}bCq4=@jNge*Z z7T>JboAyvuvXM~4wzvXeRzx@|)7>=mnz%f&JE~wPuEVvX&(HLqm?H^&$_vg9XIGOy zQEPo;RUSiRQ*?Gf{3W%?M(1C!*+J#GRMow$CzHojJ}1h^hd<(5UkLcOqndc5SF=>L z<3IP|D^89znl6P2N=czj;u~1?=jZjC5Cg52M>HT#sWcUjAAQ%G7uJB4YroLoT*!$e@+vGdoX{cJFf4=`#ReyJWqPIaiK75iybo6XNiAt@ z2xFLaj7K(X?w0m6=7T3Dy-q#nypDp-%xh>Lm1AYKd#Y`De%a(LMnh}5l0A+WLxL$5 zgA9LlZ)Mr2UU1IrHcu;jCHLK-Y_>8gtQ^i@@4tc6Z?szDTE0SAPDcb!7h0DBC6*mF z1Am62&WMgc$nY)i56J795wj%X5GV6roc^3MtF#{`8)>)E)T;6}m>)8)y{&qYxQ6fi z9)D1l!fF*S61$&#doq4?_9xD90ozCjDk+Cie??M?-lddN$NKjVhkx$XFv#AiXd?0d zS3OYS-xPJf;l*|Z8a@UI2X5ajN)eO7MJ#fxf&-QXAnXw8%grc?pPGd6!U<)MdoKj& zdojUfW-PFXLE$tdeT?6hkf&8|8E)jrIiG2T2n^|{F@HP5=goz(`Bh{ot?$Sb^h5Lk z-6wRccP+OZOUqip;#E;PLuIWn!DsTzRD=mrZQWAj@>r|sxh#u#6jn+adyz42rc6(7 z8|8S=F$J=}I(C;F5?R_V#WB0{|M62WM~F<+F9;K{(VsQEJh(0w=}GrnR}V0KxX_>( z*pp+FgND>Xctk{GmfPMHnW#epPe2lGEyAUDEI)q+gI`cbJZQuS&J?nA9^dyIk@ky& znql9<#-uA%^O$(;!QPd?hofz#@dM5cdZ})M6Hk|agM`$Ikh=!O2{D&ikpj6_+5JMR zdA=N3GWwVaH&I_JeOG3S9YqU-jq&<EclBSkgnlN8IdW2!v&OXjlSVc@TXui2>PMs>-*}{oL zMrtxkG?yM%h=(E1D~Fm37v?;%2wd~PrhuFn(Xqk1IugL%9ogN*gqh=Mp*sd#adsK7 zSI8Y63SHzY$BFgtOl74lyRy`{D4K;A`zMuQ_}KpJMBckn5Qmx=C2nw^w6qDmLT5vo zN*-tibw$k~T;wKMTiUGXpdN_8@PUil39|STe8kxyls2VsCf}cwmCS?oe*B}i_t>I{ zK%$P@xF9DdK8L`G_0bLnTYyAY4cnb~)t6M)!*xH$JUv1!_5S6~8`%D#nZcBg%#G>) z9{m?P6#O>MYX%!Vf$m&>H@0dS>Eje9`AYtRt2c|O%(fG(YcBDtgWi$I7BqmuVuj!M5anzfo)m32=sIP^Jbz*S`!KrMKGEIV@UiCdm#NN5SH@gR zVWd&w}bgtz4S$`l4mDTUv&TIb>EkZNFdHKF8O7lJJOK9XNW)#+6m9 zGUz?K3PucjAgwb0MrA}@T10Z}wpzvO7B{5NKc>($vjN_@khAu1R`?+c@~$;LF;ti@ zE!+K3KRBrL8xdID%07D190m9Hw_sfOJJ1aFP5_=@LMr85uK_$4=6r=P`KB% zIb;_v9p-VWh!yXzCdvh(d&y>pPLiHvKlYf@@@EXie8ZPNoe@{5(ym3So+R_VNbpoz~soTTao z5%~83wbp6ehR*r6*rvKO-E)_2DCkVY1vs)d)SS|t& zQcX#*-KIdWKgNA4n6VZkPo8Is;uJetWDL5&_YCfwY|Z9WC9HXHE=5p8R@98jy((6w zd=|Ib-kINY9kq?PU#`IHs7q7hQD%W+c;kXNuF8SR99-Z_LV4HAn=ivo6I3DyI zXCIMCeyM;9A8VNp2dlI?`WqBxPLZwg)H5*1QsEwENvfMwr+J>SB-l`BX}3PVb>+E+ zz!qb&Z=klJYH}HGy$W=8#&$39LGDI(pxs%K>qBtIQ@zINe6yQf+ za^o;%IebfB_WDdy`f?DYTqce})f}gJg=}sW+O2eIT95C5(6Y;35fr1!`rDx#e;m5J z>jLqN>-#qoYa@`;7BVSd=}~Y<9ujJSG0XVQLPW!ukE=Ad&rvKQ2%uE-0y-S!{%L88 zH|=^hIcO_?@B3XMfGCuU{uE2P5fiwgW4Ad@UK9-yZT0WYpdQSO12TsefHl(bKIxB7 z6VCm40bw!Eb3D^heAzyIy5aquM;oI*I z^yZ!uyWW*)_PPt-1rwFH?dNPrk-yPIvIlhp6bgfn0w95vZ;ro`livDr!%1ty2i}^qrllFha&7I%~Mwcb*%*O2t2+-9w(5lq1eg8jn$qIJ> literal 0 HcmV?d00001 diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a34301f3864675a2b09308ce64ebd482130141bc GIT binary patch literal 5560 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/kotlin/jniviewer/app/src/main/res/values/id.xml b/samples/kotlin/jniviewer/app/src/main/res/values/id.xml new file mode 100644 index 0000000000..56bde943a6 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/res/values/id.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/kotlin/jniviewer/app/src/main/res/values/strings.xml b/samples/kotlin/jniviewer/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..039a88f880 --- /dev/null +++ b/samples/kotlin/jniviewer/app/src/main/res/values/strings.xml @@ -0,0 +1,34 @@ + + + OpenCASCADE JNI Kotlin Sample + #484848 + #0099CC + #66252525 + + .png + .jpg + + + .brep + .rle + .iges + .igs + .step + .stp + + wireframe/shading + color + material + transparency + show/hide hidden lines + + OpenCASCADE JNI Kotlin Sample

+

Simple viewer for BREP, STEP and IGES files.

+

Driven by Open CASCADE Technology %d.%d.%d.

+

Copyright 2014-2021 OPEN CASCADE SAS.

+

+

https://dev.opencascade.org

+ ]]> +
+
diff --git a/samples/kotlin/jniviewer/build.gradle b/samples/kotlin/jniviewer/build.gradle new file mode 100644 index 0000000000..7252192b26 --- /dev/null +++ b/samples/kotlin/jniviewer/build.gradle @@ -0,0 +1,19 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = '1.4.32' + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:4.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + jcenter() + google() + } +} diff --git a/samples/kotlin/jniviewer/gradle.properties.template b/samples/kotlin/jniviewer/gradle.properties.template new file mode 100644 index 0000000000..2cf7323475 --- /dev/null +++ b/samples/kotlin/jniviewer/gradle.properties.template @@ -0,0 +1,5 @@ +# customized paths +OCCT_ROOT=c\:/android/occt-dev-android +FREETYPE_ROOT=c\:/android/freetype-2.7.1-android +# in case if OCCT was built with FreeImage +#FREEIMAGE_ROOT=c\:/android/freeimage-3.17-android diff --git a/samples/kotlin/jniviewer/settings.gradle b/samples/kotlin/jniviewer/settings.gradle new file mode 100644 index 0000000000..e7b4def49c --- /dev/null +++ b/samples/kotlin/jniviewer/settings.gradle @@ -0,0 +1 @@ +include ':app' -- 2.39.5