diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ebeb4aa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,54 @@ +.gitattributes eol=lf +.gitignore eol=lf +*.gradle eol=lf +*.txt eol=lf +*.h eol=lf +*.c eol=lf +*.cpp eol=lf +*.cxx eol=lf +*.hxx eol=lf +*.lxx eol=lf +*.pxx eol=lf +*.cl eol=lf +*.mm eol=lf +*.i eol=lf +*.el eol=lf +*.sh eol=lf +*.csh eol=lf +*.tcl eol=lf +*.cbp eol=lf +*.svg eol=lf +*.xib eol=lf +*.plist eol=lf +*.java eol=lf +*.igs eol=lf +*.iges eol=lf +*.stp eol=lf +*.step eol=lf +*.brep eol=lf +*.md eol=lf +*.fs eol=lf +*.vs eol=lf +*.glsl eol=lf +*.bat eol=crlf +*.cmd eol=crlf +*.rc eol=crlf +*.cs eol=crlf +*.def eol=crlf +*.ini eol=crlf +*.so binary +*.dylib binary +*.7z binary +*.pdf binary +*.png binary +*.jpg binary +*.bmp binary +*.gif binary +*.xwd binary +*.ico binary +*.icns binary +*.std binary +*.gz binary +*.doc binary +*.mft binary +*.stl binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3328b0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.gradle +/.idea +/build +/gradle +gradlew +gradlew.bat +/app/.cxx +/app/build +gradle.properties +local.properties diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..16c16e4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) 2021 OPEN CASCADE SAS + +This project 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 required OCCT headers diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..b4946c7 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,39 @@ +OCCT JniViewer Kotlin sample for Android +================== + +This sample demonstrates simple way of using OCCT libraries in Android application written using Kotlin. +The project has been converted from original Java JNI sample. + +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/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..277b8b4 --- /dev/null +++ b/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/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c25a6d6 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt b/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt new file mode 100644 index 0000000..714c187 --- /dev/null +++ b/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/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt b/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt new file mode 100644 index 0000000..bbbc119 --- /dev/null +++ b/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/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt b/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt new file mode 100644 index 0000000..f1bd685 --- /dev/null +++ b/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/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt b/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt new file mode 100644 index 0000000..3eeb7fa --- /dev/null +++ b/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/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt b/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt new file mode 100644 index 0000000..971aa19 --- /dev/null +++ b/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/app/src/main/jni/CMakeLists.txt b/app/src/main/jni/CMakeLists.txt new file mode 100644 index 0000000..59e41e6 --- /dev/null +++ b/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/app/src/main/jni/OcctJni_MsgPrinter.cxx b/app/src/main/jni/OcctJni_MsgPrinter.cxx new file mode 100644 index 0000000..2934c97 --- /dev/null +++ b/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/app/src/main/jni/OcctJni_MsgPrinter.hxx b/app/src/main/jni/OcctJni_MsgPrinter.hxx new file mode 100644 index 0000000..a52b03b --- /dev/null +++ b/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/app/src/main/jni/OcctJni_Viewer.cxx b/app/src/main/jni/OcctJni_Viewer.cxx new file mode 100644 index 0000000..59ec8e9 --- /dev/null +++ b/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/app/src/main/jni/OcctJni_Viewer.hxx b/app/src/main/jni/OcctJni_Viewer.hxx new file mode 100644 index 0000000..f56e86e --- /dev/null +++ b/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/app/src/main/res/drawable-hdpi/close_l.png b/app/src/main/res/drawable-hdpi/close_l.png new file mode 100644 index 0000000..0125c4a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/close_l.png differ diff --git a/app/src/main/res/drawable-hdpi/close_p.png b/app/src/main/res/drawable-hdpi/close_p.png new file mode 100644 index 0000000..b5ce8bd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/close_p.png differ diff --git a/app/src/main/res/drawable-hdpi/fit.png b/app/src/main/res/drawable-hdpi/fit.png new file mode 100644 index 0000000..70daee1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/fit.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..d27ba82 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/info.png b/app/src/main/res/drawable-hdpi/info.png new file mode 100644 index 0000000..88d27c8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/info.png differ diff --git a/app/src/main/res/drawable-hdpi/info_image.png b/app/src/main/res/drawable-hdpi/info_image.png new file mode 100644 index 0000000..bac9bea Binary files /dev/null and b/app/src/main/res/drawable-hdpi/info_image.png differ diff --git a/app/src/main/res/drawable-hdpi/message.png b/app/src/main/res/drawable-hdpi/message.png new file mode 100644 index 0000000..a3dc8cc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/message.png differ diff --git a/app/src/main/res/drawable-hdpi/open.png b/app/src/main/res/drawable-hdpi/open.png new file mode 100644 index 0000000..68e5265 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/open.png differ diff --git a/app/src/main/res/drawable-hdpi/open_l.png b/app/src/main/res/drawable-hdpi/open_l.png new file mode 100644 index 0000000..6069ce3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/open_l.png differ diff --git a/app/src/main/res/drawable-hdpi/open_p.png b/app/src/main/res/drawable-hdpi/open_p.png new file mode 100644 index 0000000..c0898e6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/open_p.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_back.png b/app/src/main/res/drawable-hdpi/proj_back.png new file mode 100644 index 0000000..77ab199 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_back.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_bottom.png b/app/src/main/res/drawable-hdpi/proj_bottom.png new file mode 100644 index 0000000..b0bb012 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_bottom.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_front.png b/app/src/main/res/drawable-hdpi/proj_front.png new file mode 100644 index 0000000..b93d4d3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_front.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_left.png b/app/src/main/res/drawable-hdpi/proj_left.png new file mode 100644 index 0000000..bce95df Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_left.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_right.png b/app/src/main/res/drawable-hdpi/proj_right.png new file mode 100644 index 0000000..8cdb338 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_right.png differ diff --git a/app/src/main/res/drawable-hdpi/proj_top.png b/app/src/main/res/drawable-hdpi/proj_top.png new file mode 100644 index 0000000..4ad098f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/proj_top.png differ diff --git a/app/src/main/res/drawable-hdpi/view.png b/app/src/main/res/drawable-hdpi/view.png new file mode 100644 index 0000000..76ce879 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/view.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..4b86dbf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..cd79bea Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a34301f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..e7f3ebf --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/id.xml b/app/src/main/res/values/id.xml new file mode 100644 index 0000000..56bde94 --- /dev/null +++ b/app/src/main/res/values/id.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..039a88f --- /dev/null +++ b/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/build.gradle b/build.gradle new file mode 100644 index 0000000..7252192 --- /dev/null +++ b/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/gradle.properties.template b/gradle.properties.template new file mode 100644 index 0000000..2cf7323 --- /dev/null +++ b/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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'